influxdb/authorization/http_server.go

604 lines
15 KiB
Go

package authorization
import (
"context"
"encoding/json"
"fmt"
"net/http"
"time"
"github.com/go-chi/chi"
"github.com/go-chi/chi/middleware"
"github.com/influxdata/influxdb/v2"
icontext "github.com/influxdata/influxdb/v2/context"
"github.com/influxdata/influxdb/v2/kit/platform"
"github.com/influxdata/influxdb/v2/kit/platform/errors"
kithttp "github.com/influxdata/influxdb/v2/kit/transport/http"
"go.uber.org/zap"
)
// TenantService is used to look up the Organization and User for an Authorization
type TenantService interface {
FindOrganizationByID(ctx context.Context, id platform.ID) (*influxdb.Organization, error)
FindOrganization(ctx context.Context, filter influxdb.OrganizationFilter) (*influxdb.Organization, error)
FindUserByID(ctx context.Context, id platform.ID) (*influxdb.User, error)
FindUser(ctx context.Context, filter influxdb.UserFilter) (*influxdb.User, error)
FindBucketByID(ctx context.Context, id platform.ID) (*influxdb.Bucket, error)
}
type AuthHandler struct {
chi.Router
api *kithttp.API
log *zap.Logger
authSvc influxdb.AuthorizationService
tenantService TenantService
}
// NewHTTPAuthHandler constructs a new http server.
func NewHTTPAuthHandler(log *zap.Logger, authService influxdb.AuthorizationService, tenantService TenantService) *AuthHandler {
h := &AuthHandler{
api: kithttp.NewAPI(kithttp.WithLog(log)),
log: log,
authSvc: authService,
tenantService: tenantService,
}
r := chi.NewRouter()
r.Use(
middleware.Recoverer,
middleware.RequestID,
middleware.RealIP,
)
r.Route("/", func(r chi.Router) {
r.Post("/", h.handlePostAuthorization)
r.Get("/", h.handleGetAuthorizations)
r.Route("/{id}", func(r chi.Router) {
r.Get("/", h.handleGetAuthorization)
r.Patch("/", h.handleUpdateAuthorization)
r.Delete("/", h.handleDeleteAuthorization)
})
})
h.Router = r
return h
}
const prefixAuthorization = "/api/v2/authorizations"
func (h *AuthHandler) Prefix() string {
return prefixAuthorization
}
// handlePostAuthorization is the HTTP handler for the POST /api/v2/authorizations route.
func (h *AuthHandler) handlePostAuthorization(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
a, err := decodePostAuthorizationRequest(ctx, r)
if err != nil {
h.api.Err(w, r, err)
return
}
user, err := getAuthorizedUser(r, h.tenantService)
if err != nil {
h.api.Err(w, r, influxdb.ErrUnableToCreateToken)
return
}
userID := user.ID
if a.UserID != nil && a.UserID.Valid() {
userID = *a.UserID
}
auth := a.toInfluxdb(userID)
if err := h.authSvc.CreateAuthorization(ctx, auth); err != nil {
h.api.Err(w, r, err)
return
}
perms, err := h.newPermissionsResponse(ctx, auth.Permissions)
if err != nil {
h.api.Err(w, r, err)
return
}
h.log.Debug("Auth created ", zap.String("auth", fmt.Sprint(auth)))
resp, err := h.newAuthResponse(ctx, auth, perms)
if err != nil {
h.api.Err(w, r, err)
return
}
h.api.Respond(w, r, http.StatusCreated, resp)
}
func getAuthorizedUser(r *http.Request, ts TenantService) (*influxdb.User, error) {
ctx := r.Context()
a, err := icontext.GetAuthorizer(ctx)
if err != nil {
return nil, err
}
return ts.FindUserByID(ctx, a.GetUserID())
}
type postAuthorizationRequest struct {
Status influxdb.Status `json:"status"`
OrgID platform.ID `json:"orgID"`
UserID *platform.ID `json:"userID,omitempty"`
Description string `json:"description"`
Permissions []influxdb.Permission `json:"permissions"`
}
type authResponse struct {
ID platform.ID `json:"id"`
Token string `json:"token"`
Status influxdb.Status `json:"status"`
Description string `json:"description"`
OrgID platform.ID `json:"orgID"`
Org string `json:"org"`
UserID platform.ID `json:"userID"`
User string `json:"user"`
Permissions []permissionResponse `json:"permissions"`
Links map[string]string `json:"links"`
CreatedAt time.Time `json:"createdAt"`
UpdatedAt time.Time `json:"updatedAt"`
}
// In the future, we would like only the service layer to look up the user and org to see if they are valid
// but for now we need to look up the User and Org here because the API expects the response
// to have the names of the Org and User
func (h *AuthHandler) newAuthResponse(ctx context.Context, a *influxdb.Authorization, ps []permissionResponse) (*authResponse, error) {
org, err := h.tenantService.FindOrganizationByID(ctx, a.OrgID)
if err != nil {
h.log.Info("Failed to get org", zap.String("handler", "getAuthorizations"), zap.String("orgID", a.OrgID.String()), zap.Error(err))
return nil, err
}
user, err := h.tenantService.FindUserByID(ctx, a.UserID)
if err != nil {
h.log.Info("Failed to get user", zap.String("userID", a.UserID.String()), zap.Error(err))
return nil, err
}
res := &authResponse{
ID: a.ID,
Token: a.Token,
Status: a.Status,
Description: a.Description,
OrgID: a.OrgID,
UserID: a.UserID,
User: user.Name,
Org: org.Name,
Permissions: ps,
Links: map[string]string{
"self": fmt.Sprintf("/api/v2/authorizations/%s", a.ID),
"user": fmt.Sprintf("/api/v2/users/%s", a.UserID),
},
CreatedAt: a.CreatedAt,
UpdatedAt: a.UpdatedAt,
}
return res, nil
}
func (p *postAuthorizationRequest) toInfluxdb(userID platform.ID) *influxdb.Authorization {
return &influxdb.Authorization{
OrgID: p.OrgID,
Status: p.Status,
Description: p.Description,
Permissions: p.Permissions,
UserID: userID,
}
}
func (a *authResponse) toInfluxdb() *influxdb.Authorization {
res := &influxdb.Authorization{
ID: a.ID,
Token: a.Token,
Status: a.Status,
Description: a.Description,
OrgID: a.OrgID,
UserID: a.UserID,
CRUDLog: influxdb.CRUDLog{
CreatedAt: a.CreatedAt,
UpdatedAt: a.UpdatedAt,
},
}
for _, p := range a.Permissions {
res.Permissions = append(res.Permissions, influxdb.Permission{Action: p.Action, Resource: p.Resource.Resource})
}
return res
}
type authsResponse struct {
Links map[string]string `json:"links"`
Auths []*authResponse `json:"authorizations"`
}
func newAuthsResponse(as []*authResponse) *authsResponse {
return &authsResponse{
// TODO(desa): update links to include paging and filter information
Links: map[string]string{
"self": "/api/v2/authorizations",
},
Auths: as,
}
}
func newPostAuthorizationRequest(a *influxdb.Authorization) (*postAuthorizationRequest, error) {
res := &postAuthorizationRequest{
OrgID: a.OrgID,
Description: a.Description,
Permissions: a.Permissions,
Status: a.Status,
}
if a.UserID.Valid() {
res.UserID = &a.UserID
}
res.SetDefaults()
return res, res.Validate()
}
func (p *postAuthorizationRequest) SetDefaults() {
if p.Status == "" {
p.Status = influxdb.Active
}
}
func (p *postAuthorizationRequest) Validate() error {
if len(p.Permissions) == 0 {
return &errors.Error{
Code: errors.EInvalid,
Msg: "authorization must include permissions",
}
}
for _, perm := range p.Permissions {
if err := perm.Valid(); err != nil {
return &errors.Error{
Err: err,
}
}
}
if !p.OrgID.Valid() {
return &errors.Error{
Err: platform.ErrInvalidID,
Code: errors.EInvalid,
Msg: "org id required",
}
}
if p.Status == "" {
p.Status = influxdb.Active
}
err := p.Status.Valid()
if err != nil {
return err
}
return nil
}
type permissionResponse struct {
Action influxdb.Action `json:"action"`
Resource resourceResponse `json:"resource"`
}
type resourceResponse struct {
influxdb.Resource
Name string `json:"name,omitempty"`
Organization string `json:"org,omitempty"`
}
func (h *AuthHandler) newPermissionsResponse(ctx context.Context, ps []influxdb.Permission) ([]permissionResponse, error) {
res := make([]permissionResponse, len(ps))
for i, p := range ps {
res[i] = permissionResponse{
Action: p.Action,
Resource: resourceResponse{
Resource: p.Resource,
},
}
if p.Resource.ID != nil {
name, err := h.getNameForResource(ctx, p.Resource.Type, *p.Resource.ID)
if errors.ErrorCode(err) == errors.ENotFound {
continue
}
if err != nil {
return nil, err
}
res[i].Resource.Name = name
}
if p.Resource.OrgID != nil {
name, err := h.getNameForResource(ctx, influxdb.OrgsResourceType, *p.Resource.OrgID)
if errors.ErrorCode(err) == errors.ENotFound {
continue
}
if err != nil {
return nil, err
}
res[i].Resource.Organization = name
}
}
return res, nil
}
func (h *AuthHandler) getNameForResource(ctx context.Context, resource influxdb.ResourceType, id platform.ID) (string, error) {
if err := resource.Valid(); err != nil {
return "", err
}
if ok := id.Valid(); !ok {
return "", platform.ErrInvalidID
}
switch resource {
case influxdb.BucketsResourceType:
r, err := h.tenantService.FindBucketByID(ctx, id)
if err != nil {
return "", err
}
return r.Name, nil
case influxdb.OrgsResourceType:
r, err := h.tenantService.FindOrganizationByID(ctx, id)
if err != nil {
return "", err
}
return r.Name, nil
case influxdb.UsersResourceType:
r, err := h.tenantService.FindUserByID(ctx, id)
if err != nil {
return "", err
}
return r.Name, nil
}
return "", nil
}
func decodePostAuthorizationRequest(ctx context.Context, r *http.Request) (*postAuthorizationRequest, error) {
a := &postAuthorizationRequest{}
if err := json.NewDecoder(r.Body).Decode(a); err != nil {
return nil, &errors.Error{
Code: errors.EInvalid,
Msg: "invalid json structure",
Err: err,
}
}
a.SetDefaults()
return a, a.Validate()
}
// handleGetAuthorizations is the HTTP handler for the GET /api/v2/authorizations route.
func (h *AuthHandler) handleGetAuthorizations(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
req, err := decodeGetAuthorizationsRequest(ctx, r)
if err != nil {
h.log.Info("Failed to decode request", zap.String("handler", "getAuthorizations"), zap.Error(err))
h.api.Err(w, r, err)
return
}
f := req.filter
// Look up user ID and org ID if they were not provided, but names were
if f.UserID == nil && f.User != nil {
u, err := h.tenantService.FindUser(ctx, influxdb.UserFilter{Name: f.User})
if err != nil {
h.api.Err(w, r, err)
return
}
f.UserID = &u.ID
}
if f.OrgID == nil && f.Org != nil {
o, err := h.tenantService.FindOrganization(ctx, influxdb.OrganizationFilter{Name: f.Org})
if err != nil {
h.api.Err(w, r, err)
return
}
f.OrgID = &o.ID
}
opts := influxdb.FindOptions{}
as, _, err := h.authSvc.FindAuthorizations(ctx, f, opts)
if err != nil {
h.api.Err(w, r, err)
return
}
auths := make([]*authResponse, 0, len(as))
for _, a := range as {
ps, err := h.newPermissionsResponse(ctx, a.Permissions)
if err != nil {
h.api.Err(w, r, err)
return
}
resp, err := h.newAuthResponse(ctx, a, ps)
if err != nil {
h.log.Info("Failed to create auth response", zap.String("handler", "getAuthorizations"))
continue
}
auths = append(auths, resp)
}
h.log.Debug("Auths retrieved ", zap.String("auths", fmt.Sprint(auths)))
h.api.Respond(w, r, http.StatusOK, newAuthsResponse(auths))
}
type getAuthorizationsRequest struct {
filter influxdb.AuthorizationFilter
}
func decodeGetAuthorizationsRequest(ctx context.Context, r *http.Request) (*getAuthorizationsRequest, error) {
qp := r.URL.Query()
req := &getAuthorizationsRequest{}
userID := qp.Get("userID")
if userID != "" {
id, err := platform.IDFromString(userID)
if err != nil {
return nil, err
}
req.filter.UserID = id
}
user := qp.Get("user")
if user != "" {
req.filter.User = &user
}
orgID := qp.Get("orgID")
if orgID != "" {
id, err := platform.IDFromString(orgID)
if err != nil {
return nil, err
}
req.filter.OrgID = id
}
org := qp.Get("org")
if org != "" {
req.filter.Org = &org
}
authID := qp.Get("id")
if authID != "" {
id, err := platform.IDFromString(authID)
if err != nil {
return nil, err
}
req.filter.ID = id
}
return req, nil
}
func (h *AuthHandler) handleGetAuthorization(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
id, err := platform.IDFromString(chi.URLParam(r, "id"))
if err != nil {
h.log.Info("Failed to decode request", zap.String("handler", "getAuthorization"), zap.Error(err))
h.api.Err(w, r, err)
return
}
a, err := h.authSvc.FindAuthorizationByID(ctx, *id)
if err != nil {
// Don't log here, it should already be handled by the service
h.api.Err(w, r, err)
return
}
ps, err := h.newPermissionsResponse(ctx, a.Permissions)
if err != nil {
h.api.Err(w, r, err)
return
}
h.log.Debug("Auth retrieved ", zap.String("auth", fmt.Sprint(a)))
resp, err := h.newAuthResponse(ctx, a, ps)
if err != nil {
h.api.Err(w, r, err)
return
}
h.api.Respond(w, r, http.StatusOK, resp)
}
// handleUpdateAuthorization is the HTTP handler for the PATCH /api/v2/authorizations/:id route that updates the authorization's status and desc.
func (h *AuthHandler) handleUpdateAuthorization(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
req, err := decodeUpdateAuthorizationRequest(ctx, r)
if err != nil {
h.log.Info("Failed to decode request", zap.String("handler", "updateAuthorization"), zap.Error(err))
h.api.Err(w, r, err)
return
}
a, err := h.authSvc.FindAuthorizationByID(ctx, req.ID)
if err != nil {
h.api.Err(w, r, err)
return
}
a, err = h.authSvc.UpdateAuthorization(ctx, a.ID, req.AuthorizationUpdate)
if err != nil {
h.api.Err(w, r, err)
return
}
ps, err := h.newPermissionsResponse(ctx, a.Permissions)
if err != nil {
h.api.Err(w, r, err)
return
}
h.log.Debug("Auth updated", zap.String("auth", fmt.Sprint(a)))
resp, err := h.newAuthResponse(ctx, a, ps)
if err != nil {
h.api.Err(w, r, err)
return
}
h.api.Respond(w, r, http.StatusOK, resp)
}
type updateAuthorizationRequest struct {
ID platform.ID
*influxdb.AuthorizationUpdate
}
func decodeUpdateAuthorizationRequest(ctx context.Context, r *http.Request) (*updateAuthorizationRequest, error) {
id, err := platform.IDFromString(chi.URLParam(r, "id"))
if err != nil {
return nil, err
}
upd := &influxdb.AuthorizationUpdate{}
if err := json.NewDecoder(r.Body).Decode(upd); err != nil {
return nil, err
}
return &updateAuthorizationRequest{
ID: *id,
AuthorizationUpdate: upd,
}, nil
}
// handleDeleteAuthorization is the HTTP handler for the DELETE /api/v2/authorizations/:id route.
func (h *AuthHandler) handleDeleteAuthorization(w http.ResponseWriter, r *http.Request) {
id, err := platform.IDFromString(chi.URLParam(r, "id"))
if err != nil {
h.log.Info("Failed to decode request", zap.String("handler", "deleteAuthorization"), zap.Error(err))
h.api.Err(w, r, err)
return
}
if err := h.authSvc.DeleteAuthorization(r.Context(), *id); err != nil {
// Don't log here, it should already be handled by the service
h.api.Err(w, r, err)
return
}
h.log.Debug("Auth deleted", zap.String("authID", fmt.Sprint(id)))
w.WriteHeader(http.StatusNoContent)
}