influxdb/http/auth_service.go

730 lines
19 KiB
Go

package http
import (
"context"
"encoding/json"
"errors"
"fmt"
"net/http"
"time"
"github.com/influxdata/httprouter"
"github.com/influxdata/influxdb/v2"
platcontext "github.com/influxdata/influxdb/v2/context"
"github.com/influxdata/influxdb/v2/kit/platform"
errors2 "github.com/influxdata/influxdb/v2/kit/platform/errors"
"github.com/influxdata/influxdb/v2/pkg/httpc"
"go.uber.org/zap"
)
const prefixAuthorization = "/api/v2/authorizations"
// AuthorizationBackend is all services and associated parameters required to construct
// the AuthorizationHandler.
type AuthorizationBackend struct {
errors2.HTTPErrorHandler
log *zap.Logger
AuthorizationService influxdb.AuthorizationService
OrganizationService influxdb.OrganizationService
UserService influxdb.UserService
LookupService influxdb.LookupService
}
// NewAuthorizationBackend returns a new instance of AuthorizationBackend.
func NewAuthorizationBackend(log *zap.Logger, b *APIBackend) *AuthorizationBackend {
return &AuthorizationBackend{
HTTPErrorHandler: b.HTTPErrorHandler,
log: log,
AuthorizationService: b.AuthorizationService,
OrganizationService: b.OrganizationService,
UserService: b.UserService,
LookupService: b.LookupService,
}
}
// AuthorizationHandler represents an HTTP API handler for authorizations.
type AuthorizationHandler struct {
*httprouter.Router
errors2.HTTPErrorHandler
log *zap.Logger
OrganizationService influxdb.OrganizationService
UserService influxdb.UserService
AuthorizationService influxdb.AuthorizationService
LookupService influxdb.LookupService
}
// NewAuthorizationHandler returns a new instance of AuthorizationHandler.
func NewAuthorizationHandler(log *zap.Logger, b *AuthorizationBackend) *AuthorizationHandler {
h := &AuthorizationHandler{
Router: NewRouter(b.HTTPErrorHandler),
HTTPErrorHandler: b.HTTPErrorHandler,
log: log,
AuthorizationService: b.AuthorizationService,
OrganizationService: b.OrganizationService,
UserService: b.UserService,
LookupService: b.LookupService,
}
h.HandlerFunc("POST", "/api/v2/authorizations", h.handlePostAuthorization)
h.HandlerFunc("GET", "/api/v2/authorizations", h.handleGetAuthorizations)
h.HandlerFunc("GET", "/api/v2/authorizations/:id", h.handleGetAuthorization)
h.HandlerFunc("PATCH", "/api/v2/authorizations/:id", h.handleUpdateAuthorization)
h.HandlerFunc("DELETE", "/api/v2/authorizations/:id", h.handleDeleteAuthorization)
return h
}
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"`
}
func newAuthResponse(a *influxdb.Authorization, org *influxdb.Organization, user *influxdb.User, ps []permissionResponse) *authResponse {
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
}
func (a *authResponse) toPlatform() *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 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 newPermissionsResponse(ctx context.Context, ps []influxdb.Permission, svc influxdb.LookupService) ([]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 := svc.FindResourceName(ctx, p.Resource.Type, *p.Resource.ID)
if errors2.ErrorCode(err) == errors2.ENotFound {
continue
}
if err != nil {
return nil, err
}
res[i].Resource.Name = name
}
if p.Resource.OrgID != nil {
name, err := svc.FindResourceName(ctx, influxdb.OrgsResourceType, *p.Resource.OrgID)
if errors2.ErrorCode(err) == errors2.ENotFound {
continue
}
if err != nil {
return nil, err
}
res[i].Resource.Organization = name
}
}
return res, nil
}
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,
}
}
// handlePostAuthorization is the HTTP handler for the POST /api/v2/authorizations route.
func (h *AuthorizationHandler) handlePostAuthorization(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
req, err := decodePostAuthorizationRequest(ctx, r)
if err != nil {
h.HandleHTTPError(ctx, err, w)
return
}
user, err := getAuthorizedUser(r, h.UserService)
if err != nil {
h.HandleHTTPError(ctx, influxdb.ErrUnableToCreateToken, w)
return
}
userID := user.ID
if req.UserID != nil && req.UserID.Valid() {
userID = *req.UserID
}
auth := req.toPlatform(userID)
org, err := h.OrganizationService.FindOrganizationByID(ctx, auth.OrgID)
if err != nil {
h.HandleHTTPError(ctx, influxdb.ErrUnableToCreateToken, w)
return
}
if err := h.AuthorizationService.CreateAuthorization(ctx, auth); err != nil {
h.HandleHTTPError(ctx, err, w)
return
}
perms, err := newPermissionsResponse(ctx, auth.Permissions, h.LookupService)
if err != nil {
h.HandleHTTPError(ctx, err, w)
return
}
h.log.Debug("Auth created ", zap.String("auth", fmt.Sprint(auth)))
if err := encodeResponse(ctx, w, http.StatusCreated, newAuthResponse(auth, org, user, perms)); err != nil {
logEncodingError(h.log, r, err)
return
}
}
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"`
}
func (p *postAuthorizationRequest) toPlatform(userID platform.ID) *influxdb.Authorization {
return &influxdb.Authorization{
OrgID: p.OrgID,
Status: p.Status,
Description: p.Description,
Permissions: p.Permissions,
UserID: userID,
}
}
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 &errors2.Error{
Code: errors2.EInvalid,
Msg: "authorization must include permissions",
}
}
for _, perm := range p.Permissions {
if err := perm.Valid(); err != nil {
return &errors2.Error{
Err: err,
}
}
}
if !p.OrgID.Valid() {
return &errors2.Error{
Err: platform.ErrInvalidID,
Code: errors2.EInvalid,
Msg: "org id required",
}
}
if p.Status == "" {
p.Status = influxdb.Active
}
err := p.Status.Valid()
if err != nil {
return err
}
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, &errors2.Error{
Code: errors2.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 *AuthorizationHandler) 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.HandleHTTPError(ctx, err, w)
return
}
opts := influxdb.FindOptions{}
as, _, err := h.AuthorizationService.FindAuthorizations(ctx, req.filter, opts)
if err != nil {
h.HandleHTTPError(ctx, err, w)
return
}
auths := make([]*authResponse, 0, len(as))
for _, a := range as {
o, err := h.OrganizationService.FindOrganizationByID(ctx, a.OrgID)
if err != nil {
h.log.Info("Failed to get organization", zap.String("handler", "getAuthorizations"), zap.String("orgID", a.OrgID.String()), zap.Error(err))
continue
}
u, err := h.UserService.FindUserByID(ctx, a.UserID)
if err != nil {
h.log.Info("Failed to get user", zap.String("handler", "getAuthorizations"), zap.String("userID", a.UserID.String()), zap.Error(err))
continue
}
ps, err := newPermissionsResponse(ctx, a.Permissions, h.LookupService)
if err != nil {
h.HandleHTTPError(ctx, err, w)
return
}
auths = append(auths, newAuthResponse(a, o, u, ps))
}
h.log.Debug("Auths retrieved ", zap.String("auths", fmt.Sprint(auths)))
if err := encodeResponse(ctx, w, http.StatusOK, newAuthsResponse(auths)); err != nil {
h.HandleHTTPError(ctx, err, w)
return
}
}
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
}
// handleGetAuthorization is the HTTP handler for the GET /api/v2/authorizations/:id route.
func (h *AuthorizationHandler) handleGetAuthorization(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
req, err := decodeGetAuthorizationRequest(ctx, r)
if err != nil {
h.log.Info("Failed to decode request", zap.String("handler", "getAuthorization"), zap.Error(err))
h.HandleHTTPError(ctx, err, w)
return
}
a, err := h.AuthorizationService.FindAuthorizationByID(ctx, req.ID)
if err != nil {
// Don't log here, it should already be handled by the service
h.HandleHTTPError(ctx, err, w)
return
}
o, err := h.OrganizationService.FindOrganizationByID(ctx, a.OrgID)
if err != nil {
h.HandleHTTPError(ctx, err, w)
return
}
u, err := h.UserService.FindUserByID(ctx, a.UserID)
if err != nil {
h.HandleHTTPError(ctx, err, w)
return
}
ps, err := newPermissionsResponse(ctx, a.Permissions, h.LookupService)
if err != nil {
h.HandleHTTPError(ctx, err, w)
return
}
h.log.Debug("Auth retrieved ", zap.String("auth", fmt.Sprint(a)))
if err := encodeResponse(ctx, w, http.StatusOK, newAuthResponse(a, o, u, ps)); err != nil {
h.HandleHTTPError(ctx, err, w)
return
}
}
type getAuthorizationRequest struct {
ID platform.ID
}
func decodeGetAuthorizationRequest(ctx context.Context, r *http.Request) (*getAuthorizationRequest, error) {
params := httprouter.ParamsFromContext(ctx)
id := params.ByName("id")
if id == "" {
return nil, &errors2.Error{
Code: errors2.EInvalid,
Msg: "url missing id",
}
}
var i platform.ID
if err := i.DecodeFromString(id); err != nil {
return nil, err
}
return &getAuthorizationRequest{
ID: i,
}, nil
}
// handleUpdateAuthorization is the HTTP handler for the PATCH /api/v2/authorizations/:id route that updates the authorization's status and desc.
func (h *AuthorizationHandler) 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.HandleHTTPError(ctx, err, w)
return
}
a, err := h.AuthorizationService.FindAuthorizationByID(ctx, req.ID)
if err != nil {
h.HandleHTTPError(ctx, err, w)
return
}
a, err = h.AuthorizationService.UpdateAuthorization(ctx, a.ID, req.AuthorizationUpdate)
if err != nil {
h.HandleHTTPError(ctx, err, w)
return
}
o, err := h.OrganizationService.FindOrganizationByID(ctx, a.OrgID)
if err != nil {
h.HandleHTTPError(ctx, err, w)
return
}
u, err := h.UserService.FindUserByID(ctx, a.UserID)
if err != nil {
h.HandleHTTPError(ctx, err, w)
return
}
ps, err := newPermissionsResponse(ctx, a.Permissions, h.LookupService)
if err != nil {
h.HandleHTTPError(ctx, err, w)
return
}
h.log.Debug("Auth updated", zap.String("auth", fmt.Sprint(a)))
if err := encodeResponse(ctx, w, http.StatusOK, newAuthResponse(a, o, u, ps)); err != nil {
h.HandleHTTPError(ctx, err, w)
return
}
}
type updateAuthorizationRequest struct {
ID platform.ID
*influxdb.AuthorizationUpdate
}
func decodeUpdateAuthorizationRequest(ctx context.Context, r *http.Request) (*updateAuthorizationRequest, error) {
params := httprouter.ParamsFromContext(ctx)
id := params.ByName("id")
if id == "" {
return nil, &errors2.Error{
Code: errors2.EInvalid,
Msg: "url missing id",
}
}
var i platform.ID
if err := i.DecodeFromString(id); err != nil {
return nil, err
}
upd := &influxdb.AuthorizationUpdate{}
if err := json.NewDecoder(r.Body).Decode(upd); err != nil {
return nil, err
}
return &updateAuthorizationRequest{
ID: i,
AuthorizationUpdate: upd,
}, nil
}
// handleDeleteAuthorization is the HTTP handler for the DELETE /api/v2/authorizations/:id route.
func (h *AuthorizationHandler) handleDeleteAuthorization(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
req, err := decodeDeleteAuthorizationRequest(ctx, r)
if err != nil {
h.log.Info("Failed to decode request", zap.String("handler", "deleteAuthorization"), zap.Error(err))
h.HandleHTTPError(ctx, err, w)
return
}
if err := h.AuthorizationService.DeleteAuthorization(ctx, req.ID); err != nil {
// Don't log here, it should already be handled by the service
h.HandleHTTPError(ctx, err, w)
return
}
h.log.Debug("Auth deleted", zap.String("authID", fmt.Sprint(req.ID)))
w.WriteHeader(http.StatusNoContent)
}
type deleteAuthorizationRequest struct {
ID platform.ID
}
func decodeDeleteAuthorizationRequest(ctx context.Context, r *http.Request) (*deleteAuthorizationRequest, error) {
params := httprouter.ParamsFromContext(ctx)
id := params.ByName("id")
if id == "" {
return nil, &errors2.Error{
Code: errors2.EInvalid,
Msg: "url missing id",
}
}
var i platform.ID
if err := i.DecodeFromString(id); err != nil {
return nil, err
}
return &deleteAuthorizationRequest{
ID: i,
}, nil
}
func getAuthorizedUser(r *http.Request, svc influxdb.UserService) (*influxdb.User, error) {
ctx := r.Context()
a, err := platcontext.GetAuthorizer(ctx)
if err != nil {
return nil, err
}
return svc.FindUserByID(ctx, a.GetUserID())
}
// AuthorizationService connects to Influx via HTTP using tokens to manage authorizations
type AuthorizationService struct {
Client *httpc.Client
}
var _ influxdb.AuthorizationService = (*AuthorizationService)(nil)
// FindAuthorizationByID finds the authorization against a remote influx server.
func (s *AuthorizationService) FindAuthorizationByID(ctx context.Context, id platform.ID) (*influxdb.Authorization, error) {
var b influxdb.Authorization
err := s.Client.
Get(prefixAuthorization, id.String()).
DecodeJSON(&b).
Do(ctx)
if err != nil {
return nil, err
}
return &b, nil
}
// FindAuthorizationByToken returns a single authorization by Token.
func (s *AuthorizationService) FindAuthorizationByToken(ctx context.Context, token string) (*influxdb.Authorization, error) {
return nil, errors.New("not supported in HTTP authorization service")
}
// FindAuthorizations returns a list of authorizations that match filter and the total count of matching authorizations.
// Additional options provide pagination & sorting.
func (s *AuthorizationService) FindAuthorizations(ctx context.Context, filter influxdb.AuthorizationFilter, opt ...influxdb.FindOptions) ([]*influxdb.Authorization, int, error) {
params := influxdb.FindOptionParams(opt...)
if filter.ID != nil {
params = append(params, [2]string{"id", filter.ID.String()})
}
if filter.UserID != nil {
params = append(params, [2]string{"userID", filter.UserID.String()})
}
if filter.User != nil {
params = append(params, [2]string{"user", *filter.User})
}
if filter.OrgID != nil {
params = append(params, [2]string{"orgID", filter.OrgID.String()})
}
if filter.Org != nil {
params = append(params, [2]string{"org", *filter.Org})
}
var as authsResponse
err := s.Client.
Get(prefixAuthorization).
QueryParams(params...).
DecodeJSON(&as).
Do(ctx)
if err != nil {
return nil, 0, err
}
auths := make([]*influxdb.Authorization, 0, len(as.Auths))
for _, a := range as.Auths {
auths = append(auths, a.toPlatform())
}
return auths, len(auths), nil
}
// CreateAuthorization creates a new authorization and sets b.ID with the new identifier.
func (s *AuthorizationService) CreateAuthorization(ctx context.Context, a *influxdb.Authorization) error {
newAuth, err := newPostAuthorizationRequest(a)
if err != nil {
return err
}
return s.Client.
PostJSON(newAuth, prefixAuthorization).
DecodeJSON(a).
Do(ctx)
}
// UpdateAuthorization updates the status and description if available.
func (s *AuthorizationService) UpdateAuthorization(ctx context.Context, id platform.ID, upd *influxdb.AuthorizationUpdate) (*influxdb.Authorization, error) {
var res authResponse
err := s.Client.
PatchJSON(upd, prefixAuthorization, id.String()).
DecodeJSON(&res).
Do(ctx)
if err != nil {
return nil, err
}
return res.toPlatform(), nil
}
// DeleteAuthorization removes a authorization by id.
func (s *AuthorizationService) DeleteAuthorization(ctx context.Context, id platform.ID) error {
return s.Client.
Delete(prefixAuthorization, id.String()).
Do(ctx)
}