1
0
mirror of https://github.com/raseels-repos/golang-saas-starter-kit.git synced 2025-06-17 00:17:59 +02:00

authenticator storage engines

Created a storage interface used by authenticator to support multiple
types of storage types for private keys. Added a new file storage engine
which is now the default for web-api. Migrated aws secrets manager to be optional.
This commit is contained in:
Lee Brown
2019-06-24 17:36:42 -08:00
parent 07e86cfd52
commit ca8670eadf
42 changed files with 1967 additions and 428 deletions

View File

@ -5,6 +5,8 @@ LABEL maintainer="lee@geeksinthewoods.com"
RUN apk --update --no-cache add \
git
RUN go get -u github.com/swaggo/swag/cmd/swag
# go to base project
WORKDIR $GOPATH/src/gitlab.com/geeks-accelerator/oss/saas-starter-kit/example-project
@ -26,6 +28,9 @@ COPY cmd/web-api/templates /templates
WORKDIR ./cmd/web-api
# Update the API documentation.
RUN swag init
RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix nocgo -o /gosrv .
FROM alpine:3.9

View File

@ -0,0 +1,26 @@
# SaaS Web API
Copyright 2019, Geeks Accelerator
accelerator@geeksinthewoods.com.com
## Description
Service exposes a JSON api.
## Local Installation
### Build
```bash
go build .
```
### Docker
To build using the docker file, need to be in the project root directory. `Dockerfile` references go.mod in root directory.
```bash
docker build -f cmd/web-api/Dockerfile -t saas-web-api .
```

View File

@ -0,0 +1,80 @@
// GENERATED BY THE COMMAND ABOVE; DO NOT EDIT
// This file was generated by swaggo/swag at
// 2019-06-24 15:42:25.999684 -0800 AKDT m=+0.030714022
package docs
import (
"bytes"
"github.com/alecthomas/template"
"github.com/swaggo/swag"
)
var doc = `{
"swagger": "2.0",
"info": {
"description": "This is a sample server celler server.",
"title": "SaaS Example API",
"termsOfService": "http://geeksinthewoods.com/terms",
"contact": {
"name": "API Support",
"url": "https://gitlab.com/geeks-accelerator/oss/saas-starter-kit",
"email": "support@geeksinthewoods.com"
},
"license": {
"name": "Apache 2.0",
"url": "http://www.apache.org/licenses/LICENSE-2.0.html"
},
"version": "{{.Version}}"
},
"host": "{{.Host}}",
"basePath": "{{.BasePath}}",
"paths": {},
"securityDefinitions": {
"ApiKeyAuth": {
"type": "apiKey",
"name": "Authorization",
"in": "header"
},
"BasicAuth": {
"type": "basic"
},
"OAuth2Password": {
"type": "oauth2",
"flow": "password",
"tokenUrl": "https://example.com/v1/oauth/token"
}
}
}`
type swaggerInfo struct {
Version string
Host string
BasePath string
Title string
Description string
}
// SwaggerInfo holds exported Swagger Info so clients can modify it
var SwaggerInfo swaggerInfo
type s struct{}
func (s *s) ReadDoc() string {
t, err := template.New("swagger_info").Parse(doc)
if err != nil {
return doc
}
var tpl bytes.Buffer
if err := t.Execute(&tpl, SwaggerInfo); err != nil {
return doc
}
return tpl.String()
}
func init() {
swag.Register(swag.Name, &s{})
}

View File

@ -0,0 +1,36 @@
{
"swagger": "2.0",
"info": {
"description": "This is a sample server celler server.",
"title": "SaaS Example API",
"termsOfService": "http://geeksinthewoods.com/terms",
"contact": {
"name": "API Support",
"url": "https://gitlab.com/geeks-accelerator/oss/saas-starter-kit",
"email": "support@geeksinthewoods.com"
},
"license": {
"name": "Apache 2.0",
"url": "http://www.apache.org/licenses/LICENSE-2.0.html"
},
"version": "{{.Version}}"
},
"host": "{{.Host}}",
"basePath": "{{.BasePath}}",
"paths": {},
"securityDefinitions": {
"ApiKeyAuth": {
"type": "apiKey",
"name": "Authorization",
"in": "header"
},
"BasicAuth": {
"type": "basic"
},
"OAuth2Password": {
"type": "oauth2",
"flow": "password",
"tokenUrl": "https://example.com/v1/oauth/token"
}
}
}

View File

@ -0,0 +1,27 @@
basePath: '{{.BasePath}}'
host: '{{.Host}}'
info:
contact:
email: support@geeksinthewoods.com
name: API Support
url: https://gitlab.com/geeks-accelerator/oss/saas-starter-kit
description: This is a sample server celler server.
license:
name: Apache 2.0
url: http://www.apache.org/licenses/LICENSE-2.0.html
termsOfService: http://geeksinthewoods.com/terms
title: SaaS Example API
version: '{{.Version}}'
paths: {}
securityDefinitions:
ApiKeyAuth:
in: header
name: Authorization
type: apiKey
BasicAuth:
type: basic
OAuth2Password:
flow: password
tokenUrl: https://example.com/v1/oauth/token
type: oauth2
swagger: "2.0"

View File

@ -0,0 +1,191 @@
package handlers
import (
"context"
"net/http"
"strconv"
"geeks-accelerator/oss/saas-starter-kit/example-project/internal/account"
"geeks-accelerator/oss/saas-starter-kit/example-project/internal/platform/auth"
"geeks-accelerator/oss/saas-starter-kit/example-project/internal/platform/web"
"github.com/jmoiron/sqlx"
"github.com/pkg/errors"
)
// Account represents the Account API method handler set.
type Account struct {
MasterDB *sqlx.DB
// ADD OTHER STATE LIKE THE LOGGER AND CONFIG HERE.
}
// List returns all the existing accounts in the system.
func (a *Account) Find(ctx context.Context, w http.ResponseWriter, r *http.Request, params map[string]string) error {
claims, ok := ctx.Value(auth.Key).(auth.Claims)
if !ok {
return errors.New("claims missing from context")
}
var req account.AccountFindRequest
if err := web.Decode(r, &req); err != nil {
return errors.Wrap(err, "")
}
res, err := account.Find(ctx, claims, a.MasterDB, req)
if err != nil {
return err
}
return web.RespondJson(ctx, w, res, http.StatusOK)
}
// Read returns the specified account from the system.
func (a *Account) Read(ctx context.Context, w http.ResponseWriter, r *http.Request, params map[string]string) error {
claims, ok := ctx.Value(auth.Key).(auth.Claims)
if !ok {
return errors.New("claims missing from context")
}
var includeArchived bool
if qv := r.URL.Query().Get("include-archived"); qv != "" {
var err error
includeArchived, err = strconv.ParseBool(qv)
if err != nil {
return errors.Wrapf(err, "Invalid value for include-archived : %s", qv)
}
}
res, err := account.Read(ctx, claims, a.MasterDB, params["id"], includeArchived)
if err != nil {
switch err {
case account.ErrInvalidID:
return web.NewRequestError(err, http.StatusBadRequest)
case account.ErrNotFound:
return web.NewRequestError(err, http.StatusNotFound)
case account.ErrForbidden:
return web.NewRequestError(err, http.StatusForbidden)
default:
return errors.Wrapf(err, "ID: %s", params["id"])
}
}
return web.RespondJson(ctx, w, res, http.StatusOK)
}
// Create inserts a new account into the system.
func (a *Account) Create(ctx context.Context, w http.ResponseWriter, r *http.Request, params map[string]string) error {
v, ok := ctx.Value(web.KeyValues).(*web.Values)
if !ok {
return web.NewShutdownError("web value missing from context")
}
claims, ok := ctx.Value(auth.Key).(auth.Claims)
if !ok {
return errors.New("claims missing from context")
}
var req account.AccountCreateRequest
if err := web.Decode(r, &req); err != nil {
return errors.Wrap(err, "")
}
res, err := account.Create(ctx, claims, a.MasterDB, req, v.Now)
if err != nil {
switch err {
case account.ErrForbidden:
return web.NewRequestError(err, http.StatusForbidden)
default:
return errors.Wrapf(err, "User: %+v", &req)
}
}
return web.RespondJson(ctx, w, res, http.StatusCreated)
}
// Update updates the specified account in the system.
func (a *Account) Update(ctx context.Context, w http.ResponseWriter, r *http.Request, params map[string]string) error {
v, ok := ctx.Value(web.KeyValues).(*web.Values)
if !ok {
return web.NewShutdownError("web value missing from context")
}
claims, ok := ctx.Value(auth.Key).(auth.Claims)
if !ok {
return errors.New("claims missing from context")
}
var req account.AccountUpdateRequest
if err := web.Decode(r, &req); err != nil {
return errors.Wrap(err, "")
}
req.ID = params["id"]
err := account.Update(ctx, claims, a.MasterDB, req, v.Now)
if err != nil {
switch err {
case account.ErrInvalidID:
return web.NewRequestError(err, http.StatusBadRequest)
case account.ErrNotFound:
return web.NewRequestError(err, http.StatusNotFound)
case account.ErrForbidden:
return web.NewRequestError(err, http.StatusForbidden)
default:
return errors.Wrapf(err, "Id: %s Account: %+v", params["id"], &req)
}
}
return web.RespondJson(ctx, w, nil, http.StatusNoContent)
}
// Archive soft-deletes the specified account from the system.
func (a *Account) Archive(ctx context.Context, w http.ResponseWriter, r *http.Request, params map[string]string) error {
v, ok := ctx.Value(web.KeyValues).(*web.Values)
if !ok {
return web.NewShutdownError("web value missing from context")
}
claims, ok := ctx.Value(auth.Key).(auth.Claims)
if !ok {
return errors.New("claims missing from context")
}
err := account.Archive(ctx, claims, a.MasterDB, params["id"], v.Now)
if err != nil {
switch err {
case account.ErrInvalidID:
return web.NewRequestError(err, http.StatusBadRequest)
case account.ErrNotFound:
return web.NewRequestError(err, http.StatusNotFound)
case account.ErrForbidden:
return web.NewRequestError(err, http.StatusForbidden)
default:
return errors.Wrapf(err, "Id: %s", params["id"])
}
}
return web.RespondJson(ctx, w, nil, http.StatusNoContent)
}
// Delete removes the specified account from the system.
func (a *Account) Delete(ctx context.Context, w http.ResponseWriter, r *http.Request, params map[string]string) error {
claims, ok := ctx.Value(auth.Key).(auth.Claims)
if !ok {
return errors.New("claims missing from context")
}
err := account.Delete(ctx, claims, a.MasterDB, params["id"])
if err != nil {
switch err {
case account.ErrInvalidID:
return web.NewRequestError(err, http.StatusBadRequest)
case account.ErrNotFound:
return web.NewRequestError(err, http.StatusNotFound)
case account.ErrForbidden:
return web.NewRequestError(err, http.StatusForbidden)
default:
return errors.Wrapf(err, "Id: %s", params["id"])
}
}
return web.RespondJson(ctx, w, nil, http.StatusNoContent)
}

View File

@ -6,20 +6,31 @@ import (
"geeks-accelerator/oss/saas-starter-kit/example-project/internal/platform/web"
"github.com/jmoiron/sqlx"
"github.com/pkg/errors"
"gopkg.in/DataDog/dd-trace-go.v1/contrib/go-redis/redis"
)
// Check provides support for orchestration health checks.
type Check struct {
MasterDB *sqlx.DB
Redis *redis.Client
// ADD OTHER STATE LIKE THE LOGGER IF NEEDED.
}
// Health validates the service is healthy and ready to accept requests.
func (c *Check) Health(ctx context.Context, w http.ResponseWriter, r *http.Request, params map[string]string) error {
// check postgres
_, err := c.MasterDB.Exec("SELECT 1")
if err != nil {
return err
return errors.Wrap(err, "Postgres failed")
}
// check redis
err = c.Redis.Ping().Err()
if err != nil {
return errors.Wrap(err, "Redis failed")
}
status := struct {

View File

@ -3,7 +3,9 @@ package handlers
import (
"context"
"net/http"
"strconv"
"geeks-accelerator/oss/saas-starter-kit/example-project/internal/platform/auth"
"geeks-accelerator/oss/saas-starter-kit/example-project/internal/platform/web"
"geeks-accelerator/oss/saas-starter-kit/example-project/internal/project"
"github.com/jmoiron/sqlx"
@ -18,30 +20,56 @@ type Project struct {
}
// List returns all the existing projects in the system.
func (p *Project) List(ctx context.Context, w http.ResponseWriter, r *http.Request, params map[string]string) error {
projects, err := project.List(ctx, p.MasterDB)
func (p *Project) Find(ctx context.Context, w http.ResponseWriter, r *http.Request, params map[string]string) error {
claims, ok := ctx.Value(auth.Key).(auth.Claims)
if !ok {
return errors.New("claims missing from context")
}
var req project.ProjectFindRequest
if err := web.Decode(r, &req); err != nil {
return errors.Wrap(err, "")
}
res, err := project.Find(ctx, claims, p.MasterDB, req)
if err != nil {
return err
}
return web.RespondJson(ctx, w, projects, http.StatusOK)
return web.RespondJson(ctx, w, res, http.StatusOK)
}
// Retrieve returns the specified project from the system.
func (p *Project) Retrieve(ctx context.Context, w http.ResponseWriter, r *http.Request, params map[string]string) error {
prod, err := project.Retrieve(ctx, p.MasterDB, params["id"])
// Read returns the specified project from the system.
func (p *Project) Read(ctx context.Context, w http.ResponseWriter, r *http.Request, params map[string]string) error {
claims, ok := ctx.Value(auth.Key).(auth.Claims)
if !ok {
return errors.New("claims missing from context")
}
var includeArchived bool
if qv := r.URL.Query().Get("include-archived"); qv != "" {
var err error
includeArchived, err = strconv.ParseBool(qv)
if err != nil {
return errors.Wrapf(err, "Invalid value for include-archived : %s", qv)
}
}
res, err := project.Read(ctx, claims, p.MasterDB, params["id"], includeArchived)
if err != nil {
switch err {
case project.ErrInvalidID:
return web.NewRequestError(err, http.StatusBadRequest)
case project.ErrNotFound:
return web.NewRequestError(err, http.StatusNotFound)
case project.ErrForbidden:
return web.NewRequestError(err, http.StatusForbidden)
default:
return errors.Wrapf(err, "ID: %s", params["id"])
}
}
return web.RespondJson(ctx, w, prod, http.StatusOK)
return web.RespondJson(ctx, w, res, http.StatusOK)
}
// Create inserts a new project into the system.
@ -51,17 +79,27 @@ func (p *Project) Create(ctx context.Context, w http.ResponseWriter, r *http.Req
return web.NewShutdownError("web value missing from context")
}
var np project.NewProject
if err := web.Decode(r, &np); err != nil {
claims, ok := ctx.Value(auth.Key).(auth.Claims)
if !ok {
return errors.New("claims missing from context")
}
var req project.ProjectCreateRequest
if err := web.Decode(r, &req); err != nil {
return errors.Wrap(err, "")
}
nUsr, err := project.Create(ctx, p.MasterDB, &np, v.Now)
res, err := project.Create(ctx, claims, p.MasterDB, req, v.Now)
if err != nil {
return errors.Wrapf(err, "Project: %+v", &np)
switch err {
case project.ErrForbidden:
return web.NewRequestError(err, http.StatusForbidden)
default:
return errors.Wrapf(err, "Project: %+v", &req)
}
}
return web.RespondJson(ctx, w, nUsr, http.StatusCreated)
return web.RespondJson(ctx, w, res, http.StatusCreated)
}
// Update updates the specified project in the system.
@ -71,20 +109,57 @@ func (p *Project) Update(ctx context.Context, w http.ResponseWriter, r *http.Req
return web.NewShutdownError("web value missing from context")
}
var up project.UpdateProject
if err := web.Decode(r, &up); err != nil {
return errors.Wrap(err, "")
claims, ok := ctx.Value(auth.Key).(auth.Claims)
if !ok {
return errors.New("claims missing from context")
}
err := project.Update(ctx, p.MasterDB, params["id"], up, v.Now)
var req project.ProjectUpdateRequest
if err := web.Decode(r, &req); err != nil {
return errors.Wrap(err, "")
}
req.ID = params["id"]
err := project.Update(ctx, claims, p.MasterDB, req, v.Now)
if err != nil {
switch err {
case project.ErrInvalidID:
return web.NewRequestError(err, http.StatusBadRequest)
case project.ErrNotFound:
return web.NewRequestError(err, http.StatusNotFound)
case project.ErrForbidden:
return web.NewRequestError(err, http.StatusForbidden)
default:
return errors.Wrapf(err, "ID: %s Update: %+v", params["id"], up)
return errors.Wrapf(err, "ID: %s Update: %+v", params["id"], req)
}
}
return web.RespondJson(ctx, w, nil, http.StatusNoContent)
}
// Archive soft-deletes the specified project from the system.
func (p *Project) Archive(ctx context.Context, w http.ResponseWriter, r *http.Request, params map[string]string) error {
v, ok := ctx.Value(web.KeyValues).(*web.Values)
if !ok {
return web.NewShutdownError("web value missing from context")
}
claims, ok := ctx.Value(auth.Key).(auth.Claims)
if !ok {
return errors.New("claims missing from context")
}
err := project.Archive(ctx, claims, p.MasterDB, params["id"], v.Now)
if err != nil {
switch err {
case project.ErrInvalidID:
return web.NewRequestError(err, http.StatusBadRequest)
case project.ErrNotFound:
return web.NewRequestError(err, http.StatusNotFound)
case project.ErrForbidden:
return web.NewRequestError(err, http.StatusForbidden)
default:
return errors.Wrapf(err, "Id: %s", params["id"])
}
}
@ -93,13 +168,20 @@ func (p *Project) Update(ctx context.Context, w http.ResponseWriter, r *http.Req
// Delete removes the specified project from the system.
func (p *Project) Delete(ctx context.Context, w http.ResponseWriter, r *http.Request, params map[string]string) error {
err := project.Delete(ctx, p.MasterDB, params["id"])
claims, ok := ctx.Value(auth.Key).(auth.Claims)
if !ok {
return errors.New("claims missing from context")
}
err := project.Delete(ctx, claims, p.MasterDB, params["id"])
if err != nil {
switch err {
case project.ErrInvalidID:
return web.NewRequestError(err, http.StatusBadRequest)
case project.ErrNotFound:
return web.NewRequestError(err, http.StatusNotFound)
case project.ErrForbidden:
return web.NewRequestError(err, http.StatusForbidden)
default:
return errors.Wrapf(err, "Id: %s", params["id"])
}

View File

@ -6,13 +6,15 @@ import (
"os"
"geeks-accelerator/oss/saas-starter-kit/example-project/internal/mid"
saasSwagger "geeks-accelerator/oss/saas-starter-kit/example-project/internal/mid/saas-swagger"
"geeks-accelerator/oss/saas-starter-kit/example-project/internal/platform/auth"
"geeks-accelerator/oss/saas-starter-kit/example-project/internal/platform/web"
"github.com/jmoiron/sqlx"
"gopkg.in/DataDog/dd-trace-go.v1/contrib/go-redis/redis"
)
// API returns a handler for a set of routes.
func API(shutdown chan os.Signal, log *log.Logger, masterDB *sqlx.DB, authenticator *auth.Authenticator) http.Handler {
func API(shutdown chan os.Signal, log *log.Logger, masterDB *sqlx.DB, redis *redis.Client, authenticator *auth.Authenticator) http.Handler {
// Construct the web.App which holds all routes as well as common Middleware.
app := web.NewApp(shutdown, log, mid.Trace(), mid.Logger(log), mid.Errors(log), mid.Metrics(), mid.Panics())
@ -28,24 +30,43 @@ func API(shutdown chan os.Signal, log *log.Logger, masterDB *sqlx.DB, authentica
MasterDB: masterDB,
TokenGenerator: authenticator,
}
app.Handle("GET", "/v1/users", u.List, mid.Authenticate(authenticator), mid.HasRole(auth.RoleAdmin))
app.Handle("GET", "/v1/users", u.Find, mid.Authenticate(authenticator))
app.Handle("POST", "/v1/users", u.Create, mid.Authenticate(authenticator), mid.HasRole(auth.RoleAdmin))
app.Handle("GET", "/v1/users/:id", u.Retrieve, mid.Authenticate(authenticator))
app.Handle("PUT", "/v1/users/:id", u.Update, mid.Authenticate(authenticator), mid.HasRole(auth.RoleAdmin))
app.Handle("GET", "/v1/users/:id", u.Read, mid.Authenticate(authenticator))
app.Handle("PATCH", "/v1/users/:id", u.Update, mid.Authenticate(authenticator))
app.Handle("PATCH", "/v1/users/:id/password", u.UpdatePassword, mid.Authenticate(authenticator), mid.HasRole(auth.RoleAdmin))
app.Handle("PATCH", "/v1/users/:id/archive", u.Archive, mid.Authenticate(authenticator), mid.HasRole(auth.RoleAdmin))
app.Handle("DELETE", "/v1/users/:id", u.Delete, mid.Authenticate(authenticator), mid.HasRole(auth.RoleAdmin))
app.Handle("PATCH", "/v1/users/switch-account/:accountId", u.SwitchAccount, mid.Authenticate(authenticator))
// This route is not authenticated
app.Handle("GET", "/v1/users/token", u.Token)
app.Handle("GET", "/v1/oauth/token", u.Token)
// Register project and sale endpoints.
// Register account endpoints.
a := Account{
MasterDB: masterDB,
}
app.Handle("GET", "/v1/accounts", a.Find, mid.Authenticate(authenticator))
app.Handle("POST", "/v1/accounts", a.Create, mid.Authenticate(authenticator))
app.Handle("GET", "/v1/accounts/:id", a.Read, mid.Authenticate(authenticator))
app.Handle("PATCH", "/v1/accounts/:id", a.Update, mid.Authenticate(authenticator), mid.HasRole(auth.RoleAdmin))
app.Handle("PATCH", "/v1/accounts/:id/archive", a.Archive, mid.Authenticate(authenticator), mid.HasRole(auth.RoleAdmin))
app.Handle("DELETE", "/v1/accounts/:id", a.Delete, mid.Authenticate(authenticator), mid.HasRole(auth.RoleAdmin))
// Register project.
p := Project{
MasterDB: masterDB,
}
app.Handle("GET", "/v1/projects", p.List, mid.Authenticate(authenticator))
app.Handle("POST", "/v1/projects", p.Create, mid.Authenticate(authenticator))
app.Handle("GET", "/v1/projects/:id", p.Retrieve, mid.Authenticate(authenticator))
app.Handle("PUT", "/v1/projects/:id", p.Update, mid.Authenticate(authenticator))
app.Handle("DELETE", "/v1/projects/:id", p.Delete, mid.Authenticate(authenticator))
app.Handle("GET", "/v1/projects", p.Find, mid.Authenticate(authenticator))
app.Handle("POST", "/v1/projects", p.Create, mid.Authenticate(authenticator), mid.HasRole(auth.RoleAdmin))
app.Handle("GET", "/v1/projects/:id", p.Read, mid.Authenticate(authenticator))
app.Handle("PATCH", "/v1/projects/:id", p.Update, mid.Authenticate(authenticator), mid.HasRole(auth.RoleAdmin))
app.Handle("PATCH", "/v1/projects/:id/archive", p.Archive, mid.Authenticate(authenticator), mid.HasRole(auth.RoleAdmin))
app.Handle("DELETE", "/v1/projects/:id", p.Delete, mid.Authenticate(authenticator), mid.HasRole(auth.RoleAdmin))
// Register swagger documentation.
app.Handle("GET", "/swagger/", saasSwagger.WrapHandler, mid.Authenticate(authenticator))
app.Handle("GET", "/swagger/*", saasSwagger.WrapHandler, mid.Authenticate(authenticator))
return app
}

View File

@ -3,6 +3,7 @@ package handlers
import (
"context"
"net/http"
"strconv"
"time"
"geeks-accelerator/oss/saas-starter-kit/example-project/internal/platform/auth"
@ -12,6 +13,9 @@ import (
"github.com/pkg/errors"
)
// sessionTtl defines the auth token expiration.
var sessionTtl = time.Hour * 24
// User represents the User API method handler set.
type User struct {
MasterDB *sqlx.DB
@ -21,7 +25,7 @@ type User struct {
}
// List returns all the existing users in the system.
func (u *User) List(ctx context.Context, w http.ResponseWriter, r *http.Request, params map[string]string) error {
func (u *User) Find(ctx context.Context, w http.ResponseWriter, r *http.Request, params map[string]string) error {
claims, ok := ctx.Value(auth.Key).(auth.Claims)
if !ok {
return errors.New("claims missing from context")
@ -32,22 +36,160 @@ func (u *User) List(ctx context.Context, w http.ResponseWriter, r *http.Request,
return errors.Wrap(err, "")
}
usrs, err := user.Find(ctx, claims, u.MasterDB, req)
res, err := user.Find(ctx, claims, u.MasterDB, req)
if err != nil {
return err
}
return web.RespondJson(ctx, w, usrs, http.StatusOK)
return web.RespondJson(ctx, w, res, http.StatusOK)
}
// Retrieve returns the specified user from the system.
func (u *User) Retrieve(ctx context.Context, w http.ResponseWriter, r *http.Request, params map[string]string) error {
// Read returns the specified user from the system.
func (u *User) Read(ctx context.Context, w http.ResponseWriter, r *http.Request, params map[string]string) error {
claims, ok := ctx.Value(auth.Key).(auth.Claims)
if !ok {
return errors.New("claims missing from context")
}
usr, err := user.FindById(ctx, claims, u.MasterDB, params["id"], false)
var includeArchived bool
if qv := r.URL.Query().Get("include-archived"); qv != "" {
var err error
includeArchived, err = strconv.ParseBool(qv)
if err != nil {
return errors.Wrapf(err, "Invalid value for include-archived : %s", qv)
}
}
res, err := user.Read(ctx, claims, u.MasterDB, params["id"], includeArchived)
if err != nil {
switch err {
case user.ErrInvalidID:
return web.NewRequestError(err, http.StatusBadRequest)
case user.ErrNotFound:
return web.NewRequestError(err, http.StatusNotFound)
case user.ErrForbidden:
return web.NewRequestError(err, http.StatusForbidden)
default:
return errors.Wrapf(err, "ID: %s", params["id"])
}
}
return web.RespondJson(ctx, w, res, http.StatusOK)
}
// Create inserts a new user into the system.
func (u *User) Create(ctx context.Context, w http.ResponseWriter, r *http.Request, params map[string]string) error {
v, ok := ctx.Value(web.KeyValues).(*web.Values)
if !ok {
return web.NewShutdownError("web value missing from context")
}
claims, ok := ctx.Value(auth.Key).(auth.Claims)
if !ok {
return errors.New("claims missing from context")
}
var req user.UserCreateRequest
if err := web.Decode(r, &req); err != nil {
return errors.Wrap(err, "")
}
res, err := user.Create(ctx, claims, u.MasterDB, req, v.Now)
if err != nil {
switch err {
case user.ErrForbidden:
return web.NewRequestError(err, http.StatusForbidden)
default:
return errors.Wrapf(err, "User: %+v", &req)
}
}
return web.RespondJson(ctx, w, res, http.StatusCreated)
}
// Update updates the specified user in the system.
func (u *User) Update(ctx context.Context, w http.ResponseWriter, r *http.Request, params map[string]string) error {
v, ok := ctx.Value(web.KeyValues).(*web.Values)
if !ok {
return web.NewShutdownError("web value missing from context")
}
claims, ok := ctx.Value(auth.Key).(auth.Claims)
if !ok {
return errors.New("claims missing from context")
}
var req user.UserUpdateRequest
if err := web.Decode(r, &req); err != nil {
return errors.Wrap(err, "")
}
req.ID = params["id"]
err := user.Update(ctx, claims, u.MasterDB, req, v.Now)
if err != nil {
switch err {
case user.ErrInvalidID:
return web.NewRequestError(err, http.StatusBadRequest)
case user.ErrNotFound:
return web.NewRequestError(err, http.StatusNotFound)
case user.ErrForbidden:
return web.NewRequestError(err, http.StatusForbidden)
default:
return errors.Wrapf(err, "Id: %s User: %+v", params["id"], &req)
}
}
return web.RespondJson(ctx, w, nil, http.StatusNoContent)
}
// Update updates the password for a specified user in the system.
func (u *User) UpdatePassword(ctx context.Context, w http.ResponseWriter, r *http.Request, params map[string]string) error {
v, ok := ctx.Value(web.KeyValues).(*web.Values)
if !ok {
return web.NewShutdownError("web value missing from context")
}
claims, ok := ctx.Value(auth.Key).(auth.Claims)
if !ok {
return errors.New("claims missing from context")
}
var req user.UserUpdatePasswordRequest
if err := web.Decode(r, &req); err != nil {
return errors.Wrap(err, "")
}
req.ID = params["id"]
err := user.UpdatePassword(ctx, claims, u.MasterDB, req, v.Now)
if err != nil {
switch err {
case user.ErrInvalidID:
return web.NewRequestError(err, http.StatusBadRequest)
case user.ErrNotFound:
return web.NewRequestError(err, http.StatusNotFound)
case user.ErrForbidden:
return web.NewRequestError(err, http.StatusForbidden)
default:
return errors.Wrapf(err, "Id: %s User: %+v", params["id"], &req)
}
}
return web.RespondJson(ctx, w, nil, http.StatusNoContent)
}
// Archive soft-deletes the specified user from the system.
func (u *User) Archive(ctx context.Context, w http.ResponseWriter, r *http.Request, params map[string]string) error {
v, ok := ctx.Value(web.KeyValues).(*web.Values)
if !ok {
return web.NewShutdownError("web value missing from context")
}
claims, ok := ctx.Value(auth.Key).(auth.Claims)
if !ok {
return errors.New("claims missing from context")
}
err := user.Archive(ctx, claims, u.MasterDB, params["id"], v.Now)
if err != nil {
switch err {
case user.ErrInvalidID:
@ -61,66 +203,6 @@ func (u *User) Retrieve(ctx context.Context, w http.ResponseWriter, r *http.Requ
}
}
return web.RespondJson(ctx, w, usr, http.StatusOK)
}
// Create inserts a new user into the system.
func (u *User) Create(ctx context.Context, w http.ResponseWriter, r *http.Request, params map[string]string) error {
claims, ok := ctx.Value(auth.Key).(auth.Claims)
if !ok {
return errors.New("claims missing from context")
}
v, ok := ctx.Value(web.KeyValues).(*web.Values)
if !ok {
return web.NewShutdownError("web value missing from context")
}
var newU user.CreateUserRequest
if err := web.Decode(r, &newU); err != nil {
return errors.Wrap(err, "")
}
usr, err := user.Create(ctx, claims, u.MasterDB, newU, v.Now)
if err != nil {
return errors.Wrapf(err, "User: %+v", &usr)
}
return web.RespondJson(ctx, w, usr, http.StatusCreated)
}
// Update updates the specified user in the system.
func (u *User) Update(ctx context.Context, w http.ResponseWriter, r *http.Request, params map[string]string) error {
claims, ok := ctx.Value(auth.Key).(auth.Claims)
if !ok {
return errors.New("claims missing from context")
}
v, ok := ctx.Value(web.KeyValues).(*web.Values)
if !ok {
return web.NewShutdownError("web value missing from context")
}
var upd user.UpdateUserRequest
if err := web.Decode(r, &upd); err != nil {
return errors.Wrap(err, "")
}
err := user.Update(ctx, claims, u.MasterDB, upd, v.Now)
if err != nil {
switch err {
case user.ErrInvalidID:
return web.NewRequestError(err, http.StatusBadRequest)
case user.ErrNotFound:
return web.NewRequestError(err, http.StatusNotFound)
case user.ErrForbidden:
return web.NewRequestError(err, http.StatusForbidden)
default:
return errors.Wrapf(err, "Id: %s User: %+v", params["id"], &upd)
}
}
return web.RespondJson(ctx, w, nil, http.StatusNoContent)
}
@ -148,6 +230,31 @@ func (u *User) Delete(ctx context.Context, w http.ResponseWriter, r *http.Reques
return web.RespondJson(ctx, w, nil, http.StatusNoContent)
}
// SwitchAccount updates the claims.
func (u *User) SwitchAccount(ctx context.Context, w http.ResponseWriter, r *http.Request, params map[string]string) error {
v, ok := ctx.Value(web.KeyValues).(*web.Values)
if !ok {
return web.NewShutdownError("web value missing from context")
}
claims, ok := ctx.Value(auth.Key).(auth.Claims)
if !ok {
return errors.New("claims missing from context")
}
tkn, err := user.SwitchAccount(ctx, u.MasterDB, u.TokenGenerator, claims, params["accountId"], sessionTtl, v.Now)
if err != nil {
switch err {
case user.ErrAuthenticationFailure:
return web.NewRequestError(err, http.StatusUnauthorized)
default:
return errors.Wrap(err, "switch account")
}
}
return web.RespondJson(ctx, w, tkn, http.StatusNoContent)
}
// Token handles a request to authenticate a user. It expects a request using
// Basic Auth with a user's email and password. It responds with a JWT.
func (u *User) Token(ctx context.Context, w http.ResponseWriter, r *http.Request, params map[string]string) error {
@ -162,8 +269,7 @@ func (u *User) Token(ctx context.Context, w http.ResponseWriter, r *http.Request
return web.NewRequestError(err, http.StatusUnauthorized)
}
// TODO Constant for token lifespan?
tkn, err := user.Authenticate(ctx, u.MasterDB, u.TokenGenerator, email, pass, time.Hour * 24, v.Now)
tkn, err := user.Authenticate(ctx, u.MasterDB, u.TokenGenerator, email, pass, sessionTtl, v.Now)
if err != nil {
switch err {
case user.ErrAuthenticationFailure:

View File

@ -5,7 +5,6 @@ import (
"encoding/json"
"expvar"
"fmt"
"gopkg.in/DataDog/dd-trace-go.v1/ddtrace/tracer"
"log"
"net/http"
_ "net/http/pprof"
@ -16,6 +15,7 @@ import (
"syscall"
"time"
"geeks-accelerator/oss/saas-starter-kit/example-project/cmd/web-api/docs"
"geeks-accelerator/oss/saas-starter-kit/example-project/cmd/web-api/handlers"
"geeks-accelerator/oss/saas-starter-kit/example-project/internal/platform/auth"
"geeks-accelerator/oss/saas-starter-kit/example-project/internal/platform/flag"
@ -29,6 +29,7 @@ import (
sqltrace "gopkg.in/DataDog/dd-trace-go.v1/contrib/database/sql"
redistrace "gopkg.in/DataDog/dd-trace-go.v1/contrib/go-redis/redis"
sqlxtrace "gopkg.in/DataDog/dd-trace-go.v1/contrib/jmoiron/sqlx"
"gopkg.in/DataDog/dd-trace-go.v1/ddtrace/tracer"
)
// build is the git version of this program. It is set using build flags in the makefile.
@ -39,6 +40,26 @@ var build = "develop"
// ie: export WEB_API_ENV=dev
var service = "WEB_API"
// @title SaaS Example API
// @description This is a sample server celler server.
// @termsOfService http://geeksinthewoods.com/terms
// @contact.name API Support
// @contact.email support@geeksinthewoods.com
// @contact.url https://gitlab.com/geeks-accelerator/oss/saas-starter-kit
// @license.name Apache 2.0
// @license.url http://www.apache.org/licenses/LICENSE-2.0.html
// @securityDefinitions.basic BasicAuth
// @securityDefinitions.apikey ApiKeyAuth
// @in header
// @name Authorization
// @securitydefinitions.oauth2.password OAuth2Password
// @tokenUrl https://example.com/v1/oauth/token
func main() {
// =========================================================================
@ -98,8 +119,9 @@ func main() {
UseRole bool `envconfig:"AWS_USE_ROLE"`
}
Auth struct {
AwsSecretID string `default:"auth-secret-key" envconfig:"AWS_SECRET_ID"`
KeyExpiration time.Duration `default:"3600s" envconfig:"KEY_EXPIRATION"`
UseAwsSecretManager bool `default:false envconfig:"USE_AWS_SECRET_MANAGER"`
AwsSecretID string `default:"auth-secret-key" envconfig:"AWS_SECRET_ID"`
KeyExpiration time.Duration `default:"3600s" envconfig:"KEY_EXPIRATION"`
}
BuildInfo struct {
CiCommitRefName string `envconfig:"CI_COMMIT_REF_NAME"`
@ -252,8 +274,13 @@ func main() {
defer masterDb.Close()
// =========================================================================
// Load auth keys from AWS and init new Authenticator
authenticator, err := auth.NewAuthenticator(awsSession, cfg.Auth.AwsSecretID, time.Now().UTC(), cfg.Auth.KeyExpiration)
// Init new Authenticator
var authenticator *auth.Authenticator
if cfg.Auth.UseAwsSecretManager {
authenticator, err = auth.NewAuthenticatorAws(awsSession, cfg.Auth.AwsSecretID, time.Now().UTC(), cfg.Auth.KeyExpiration)
} else {
authenticator, err = auth.NewAuthenticatorFile("", time.Now().UTC(), cfg.Auth.KeyExpiration)
}
if err != nil {
log.Fatalf("main : Constructing authenticator : %v", err)
}
@ -282,6 +309,19 @@ func main() {
// =========================================================================
// Start API Service
// Programmatically set swagger info.
{
docs.SwaggerInfo.Version = build
u, err := url.Parse(cfg.App.BaseUrl)
if err != nil {
log.Fatalf("main : Parse app base url %s : %v", cfg.App.BaseUrl, err)
}
docs.SwaggerInfo.Host = u.Host
docs.SwaggerInfo.BasePath = "/v1"
}
// Make a channel to listen for an interrupt or terminate signal from the OS.
// Use a buffered channel because the signal package requires it.
shutdown := make(chan os.Signal, 1)
@ -289,7 +329,7 @@ func main() {
api := http.Server{
Addr: cfg.HTTP.Host,
Handler: handlers.API(shutdown, log, masterDb, authenticator),
Handler: handlers.API(shutdown, log, masterDb, redisClient, authenticator),
ReadTimeout: cfg.HTTP.ReadTimeout,
WriteTimeout: cfg.HTTP.WriteTimeout,
MaxHeaderBytes: 1 << 20,

View File

@ -30,6 +30,8 @@ func TestProjects(t *testing.T) {
t.Run("crudProjects", crudProject)
}
// TODO: need to test Archive
// getProjects200Empty validates an empty projects list can be retrieved with the endpoint.
func getProjects200Empty(t *testing.T) {
r := httptest.NewRequest("GET", "/v1/projects", nil)