diff --git a/auth.go b/auth.go index fdefeb4709..b339d14801 100644 --- a/auth.go +++ b/auth.go @@ -9,6 +9,7 @@ import ( type Authorization struct { ID ID `json:"id"` Token string `json:"token"` + Disabled bool `json:"disabled"` User string `json:"user,omitempty"` UserID ID `json:"userID,omitempty"` Permissions []Permission `json:"permissions"` @@ -29,6 +30,12 @@ type AuthorizationService interface { // Creates a new authorization and sets a.Token and a.UserID with the new identifier. CreateAuthorization(ctx context.Context, a *Authorization) error + // DisableAuthorization updates the token to be disabled. + DisableAuthorization(ctx context.Context, id ID) error + + // EnableAuthorization updates the token to be enabled. + EnableAuthorization(ctx context.Context, id ID) error + // Removes a authorization by token. DeleteAuthorization(ctx context.Context, id ID) error } @@ -85,19 +92,19 @@ func (p Permission) String() string { } var ( - // CreateUser is a permission for creating users. + // CreateUserPermission is a permission for creating users. CreateUserPermission = Permission{ Action: CreateAction, Resource: UserResource, } - // DeleteUser is a permission for deleting users. + // DeleteUserPermission is a permission for deleting users. DeleteUserPermission = Permission{ Action: DeleteAction, Resource: UserResource, } ) -// ReadBucket constructs a permission for reading a bucket. +// ReadBucketPermission constructs a permission for reading a bucket. func ReadBucketPermission(id ID) Permission { return Permission{ Action: ReadAction, @@ -105,7 +112,7 @@ func ReadBucketPermission(id ID) Permission { } } -// WriteBucket constructs a permission for writing to a bucket. +// WriteBucketPermission constructs a permission for writing to a bucket. func WriteBucketPermission(id ID) Permission { return Permission{ Action: WriteAction, @@ -113,9 +120,14 @@ func WriteBucketPermission(id ID) Permission { } } -// Allowed returns true if the permission exists in a list of permissions. -func Allowed(req Permission, ps []Permission) bool { - for _, p := range ps { +// Allowed returns true if the authorization is enabled and requested permission level +// exists in the authorization's list of permissions. +func Allowed(req Permission, auth *Authorization) bool { + if auth.Disabled { + return false + } + + for _, p := range auth.Permissions { if p.Action == req.Action && p.Resource == req.Resource { return true } diff --git a/bolt/authorization.go b/bolt/authorization.go index ee013190e6..10a08759cc 100644 --- a/bolt/authorization.go +++ b/bolt/authorization.go @@ -281,3 +281,34 @@ func (c *Client) deleteAuthorization(ctx context.Context, tx *bolt.Tx, id platfo } return tx.Bucket(authorizationBucket).Delete(id) } + +// DisableAuthorization disables the authorization by setting the disabled field to true. +func (c *Client) DisableAuthorization(ctx context.Context, id platform.ID) error { + return c.db.Update(func(tx *bolt.Tx) error { + isDisabled := true + return c.updateAuthorization(ctx, tx, id, isDisabled) + }) +} + +// EnableAuthorization enables the authorization by setting the disabled field to false. +func (c *Client) EnableAuthorization(ctx context.Context, id platform.ID) error { + return c.db.Update(func(tx *bolt.Tx) error { + isDisabled := false + return c.updateAuthorization(ctx, tx, id, isDisabled) + }) +} + +func (c *Client) updateAuthorization(ctx context.Context, tx *bolt.Tx, id platform.ID, isDisabled bool) error { + a, err := c.findAuthorizationByID(ctx, tx, id) + if err != nil { + return err + } + + a.Disabled = isDisabled + b, err := json.Marshal(a) + if err != nil { + return err + } + + return tx.Bucket(authorizationBucket).Put(a.ID, b) +} diff --git a/http/auth_service.go b/http/auth_service.go index 090ddcc368..db93557b1e 100644 --- a/http/auth_service.go +++ b/http/auth_service.go @@ -32,6 +32,7 @@ func NewAuthorizationHandler() *AuthorizationHandler { h.HandlerFunc("POST", "/v1/authorizations", h.handlePostAuthorization) h.HandlerFunc("GET", "/v1/authorizations", h.handleGetAuthorizations) h.HandlerFunc("GET", "/v1/authorizations/:id", h.handleGetAuthorization) + h.HandlerFunc("PATCH", "/v1/authorizations/:id", h.handleEnableDisableAuthorization) h.HandlerFunc("DELETE", "/v1/authorizations/:id", h.handleDeleteAuthorization) return h } @@ -181,6 +182,79 @@ func decodeGetAuthorizationRequest(ctx context.Context, r *http.Request) (*getAu }, nil } +// handleEnableDisableAuthorization is the HTTP handler for the PATCH /v1/authorizations/:id route. +func (h *AuthorizationHandler) handleEnableDisableAuthorization(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + + req, err := decodeUpdateAuthorizationRequest(ctx, r) + if err != nil { + h.Logger.Info("failed to decode request", zap.String("handler", "updateAuthorization"), zap.Error(err)) + EncodeError(ctx, err, w) + return + } + + a, err := h.AuthorizationService.FindAuthorizationByID(ctx, req.ID) + if err != nil { + EncodeError(ctx, err, w) + return + } + + if req.Disabled == a.Disabled { + if err := encodeResponse(ctx, w, http.StatusOK, a); err != nil { + h.Logger.Info("failed to encode response", zap.String("handler", "updateAuthorization"), zap.Error(err)) + EncodeError(ctx, err, w) + return + } + return + } + + if req.Disabled { + if err := h.AuthorizationService.DisableAuthorization(ctx, req.ID); err != nil { + EncodeError(ctx, err, w) + return + } + } else { + if err := h.AuthorizationService.EnableAuthorization(ctx, req.ID); err != nil { + EncodeError(ctx, err, w) + return + } + } + + if err := encodeResponse(ctx, w, http.StatusOK, a); err != nil { + h.Logger.Info("failed to encode response", zap.String("handler", "updateAuthorization"), zap.Error(err)) + EncodeError(ctx, err, w) + return + } +} + +type updateAuthorizationRequest struct { + ID platform.ID + Disabled bool +} + +func decodeUpdateAuthorizationRequest(ctx context.Context, r *http.Request) (*updateAuthorizationRequest, error) { + params := httprouter.ParamsFromContext(ctx) + id := params.ByName("id") + if id == "" { + return nil, kerrors.InvalidDataf("url missing id") + } + + var i platform.ID + if err := i.DecodeFromString(id); err != nil { + return nil, err + } + + a := &platform.Authorization{} + if err := json.NewDecoder(r.Body).Decode(a); err != nil { + return nil, err + } + + return &updateAuthorizationRequest{ + ID: i, + Disabled: a.Disabled, + }, nil +} + // handleDeleteAuthorization is the HTTP handler for the DELETE /v1/authorizations/:id route. func (h *AuthorizationHandler) handleDeleteAuthorization(w http.ResponseWriter, r *http.Request) { ctx := r.Context() @@ -231,6 +305,7 @@ type AuthorizationService struct { var _ platform.AuthorizationService = (*AuthorizationService)(nil) +// FindAuthorizationByID finds the authorization against a remote influx server. func (s *AuthorizationService) FindAuthorizationByID(ctx context.Context, id platform.ID) (*platform.Authorization, error) { u, err := newURL(s.Addr, authorizationIDPath(id)) if err != nil { @@ -241,7 +316,7 @@ func (s *AuthorizationService) FindAuthorizationByID(ctx context.Context, id pla if err != nil { return nil, err } - req.Header.Set("Authorization", s.Token) + SetToken(s.Token, req) hc := newClient(u.Scheme, s.InsecureSkipVerify) resp, err := hc.Do(req) @@ -295,7 +370,7 @@ func (s *AuthorizationService) FindAuthorizations(ctx context.Context, filter pl } req.URL.RawQuery = query.Encode() - req.Header.Set("Authorization", s.Token) + SetToken(s.Token, req) hc := newClient(u.Scheme, s.InsecureSkipVerify) resp, err := hc.Do(req) @@ -338,7 +413,7 @@ func (s *AuthorizationService) CreateAuthorization(ctx context.Context, a *platf } req.Header.Set("Content-Type", "application/json") - req.Header.Set("Authorization", s.Token) + SetToken(s.Token, req) hc := newClient(u.Scheme, s.InsecureSkipVerify) @@ -359,6 +434,58 @@ func (s *AuthorizationService) CreateAuthorization(ctx context.Context, a *platf return nil } +type enableDisableAuthorizationRequest struct { + Disabled bool `json:"disabled"` +} + +// updateAuthorization enables or disables authorization. +func (s *AuthorizationService) updateAuthorization(ctx context.Context, id platform.ID, disabled bool) error { + u, err := newURL(s.Addr, authorizationIDPath(id)) + if err != nil { + return err + } + + b, err := json.Marshal(enableDisableAuthorizationRequest{ + Disabled: disabled, + }) + if err != nil { + return err + } + + req, err := http.NewRequest("PATCH", u.String(), bytes.NewReader(b)) + if err != nil { + return err + } + + req.Header.Set("Content-Type", "application/json") + SetToken(s.Token, req) + + hc := newClient(u.Scheme, s.InsecureSkipVerify) + + resp, err := hc.Do(req) + if err != nil { + return err + } + + if err := CheckError(resp); err != nil { + return err + } + + return nil +} + +// EnableAuthorization enables a disabled authorization. +func (s *AuthorizationService) EnableAuthorization(ctx context.Context, id platform.ID) error { + isDisabled := false + return s.updateAuthorization(ctx, id, isDisabled) +} + +// DisableAuthorization disables an enabled authorization. +func (s *AuthorizationService) DisableAuthorization(ctx context.Context, id platform.ID) error { + isDisabled := true + return s.updateAuthorization(ctx, id, isDisabled) +} + // DeleteAuthorization removes a authorization by id. func (s *AuthorizationService) DeleteAuthorization(ctx context.Context, id platform.ID) error { u, err := newURL(s.Addr, authorizationIDPath(id)) @@ -370,7 +497,7 @@ func (s *AuthorizationService) DeleteAuthorization(ctx context.Context, id platf if err != nil { return err } - req.Header.Set("Authorization", s.Token) + SetToken(s.Token, req) hc := newClient(u.Scheme, s.InsecureSkipVerify) resp, err := hc.Do(req) diff --git a/http/bucket_service.go b/http/bucket_service.go index f241f3cedb..e44ee9f56a 100644 --- a/http/bucket_service.go +++ b/http/bucket_service.go @@ -281,7 +281,7 @@ func (s *BucketService) FindBucketByID(ctx context.Context, id platform.ID) (*pl if err != nil { return nil, err } - req.Header.Set("Authorization", s.Token) + SetToken(s.Token, req) hc := newClient(u.Scheme, s.InsecureSkipVerify) resp, err := hc.Do(req) @@ -344,7 +344,7 @@ func (s *BucketService) FindBuckets(ctx context.Context, filter platform.BucketF } req.URL.RawQuery = query.Encode() - req.Header.Set("Authorization", s.Token) + SetToken(s.Token, req) hc := newClient(u.Scheme, s.InsecureSkipVerify) resp, err := hc.Do(req) @@ -383,7 +383,7 @@ func (s *BucketService) CreateBucket(ctx context.Context, b *platform.Bucket) er } req.Header.Set("Content-Type", "application/json") - req.Header.Set("Authorization", s.Token) + SetToken(s.Token, req) hc := newClient(u.Scheme, s.InsecureSkipVerify) @@ -423,7 +423,7 @@ func (s *BucketService) UpdateBucket(ctx context.Context, id platform.ID, upd pl } req.Header.Set("Content-Type", "application/json") - req.Header.Set("Authorization", s.Token) + SetToken(s.Token, req) hc := newClient(u.Scheme, s.InsecureSkipVerify) @@ -456,7 +456,7 @@ func (s *BucketService) DeleteBucket(ctx context.Context, id platform.ID) error if err != nil { return err } - req.Header.Set("Authorization", s.Token) + SetToken(s.Token, req) hc := newClient(u.Scheme, s.InsecureSkipVerify) resp, err := hc.Do(req) diff --git a/http/org_service.go b/http/org_service.go index 166aa6ebf6..847a2f7583 100644 --- a/http/org_service.go +++ b/http/org_service.go @@ -298,7 +298,7 @@ func (s *OrganizationService) FindOrganizations(ctx context.Context, filter plat return nil, 0, err } - req.Header.Set("Authorization", s.Token) + SetToken(s.Token, req) hc := newClient(url.Scheme, s.InsecureSkipVerify) resp, err := hc.Do(req) @@ -341,7 +341,7 @@ func (s *OrganizationService) CreateOrganization(ctx context.Context, o *platfor } req.Header.Set("Content-Type", "application/json") - req.Header.Set("Authorization", s.Token) + SetToken(s.Token, req) hc := newClient(url.Scheme, s.InsecureSkipVerify) @@ -379,7 +379,7 @@ func (s *OrganizationService) UpdateOrganization(ctx context.Context, id platfor } req.Header.Set("Content-Type", "application/json") - req.Header.Set("Authorization", s.Token) + SetToken(s.Token, req) hc := newClient(u.Scheme, s.InsecureSkipVerify) @@ -411,7 +411,7 @@ func (s *OrganizationService) DeleteOrganization(ctx context.Context, id platfor if err != nil { return err } - req.Header.Set("Authorization", s.Token) + SetToken(s.Token, req) hc := newClient(u.Scheme, s.InsecureSkipVerify) resp, err := hc.Do(req) diff --git a/http/platform_handler.go b/http/platform_handler.go index 75b2600714..2df3f775f8 100644 --- a/http/platform_handler.go +++ b/http/platform_handler.go @@ -143,7 +143,7 @@ func (h *PlatformHandler) PrometheusCollectors() []prometheus.Collector { } func extractAuthorization(ctx context.Context, r *nethttp.Request) (context.Context, error) { - t, err := ParseAuthHeaderToken(r) + t, err := GetToken(r) if err != nil { return ctx, err } diff --git a/http/proxy_query_service.go b/http/proxy_query_service.go index a5e3b69cf6..c95d0781f9 100644 --- a/http/proxy_query_service.go +++ b/http/proxy_query_service.go @@ -103,7 +103,7 @@ func (s *ProxyQueryService) Query(ctx context.Context, w io.Writer, req *query.P if err != nil { return 0, err } - hreq.Header.Set("Authorization", s.Token) + SetToken(s.Token, hreq) hreq = hreq.WithContext(ctx) hc := newClient(u.Scheme, s.InsecureSkipVerify) diff --git a/http/query_service.go b/http/query_service.go index c49fea027e..2276e26b34 100644 --- a/http/query_service.go +++ b/http/query_service.go @@ -139,7 +139,7 @@ func (s *QueryService) Query(ctx context.Context, req *query.Request) (query.Res if err != nil { return nil, err } - hreq.Header.Set("Authorization", s.Token) + SetToken(s.Token, hreq) hreq = hreq.WithContext(ctx) hc := newClient(u.Scheme, s.InsecureSkipVerify) diff --git a/http/source_service.go b/http/source_service.go index bc3a5c61ed..b848351a3a 100644 --- a/http/source_service.go +++ b/http/source_service.go @@ -467,7 +467,7 @@ func (s *SourceService) FindSourceByID(ctx context.Context, id platform.ID) (*pl if err != nil { return nil, err } - req.Header.Set("Authorization", s.Token) + SetToken(s.Token, req) hc := newClient(u.Scheme, s.InsecureSkipVerify) resp, err := hc.Do(req) @@ -501,7 +501,7 @@ func (s *SourceService) FindSources(ctx context.Context, opt platform.FindOption return nil, 0, err } - req.Header.Set("Authorization", s.Token) + SetToken(s.Token, req) hc := newClient(u.Scheme, s.InsecureSkipVerify) resp, err := hc.Do(req) @@ -540,7 +540,7 @@ func (s *SourceService) CreateSource(ctx context.Context, b *platform.Source) er } req.Header.Set("Content-Type", "application/json") - req.Header.Set("Authorization", s.Token) + SetToken(s.Token, req) hc := newClient(u.Scheme, s.InsecureSkipVerify) @@ -580,7 +580,7 @@ func (s *SourceService) UpdateSource(ctx context.Context, id platform.ID, upd pl } req.Header.Set("Content-Type", "application/json") - req.Header.Set("Authorization", s.Token) + SetToken(s.Token, req) hc := newClient(u.Scheme, s.InsecureSkipVerify) @@ -613,7 +613,7 @@ func (s *SourceService) DeleteSource(ctx context.Context, id platform.ID) error if err != nil { return err } - req.Header.Set("Authorization", s.Token) + SetToken(s.Token, req) hc := newClient(u.Scheme, s.InsecureSkipVerify) resp, err := hc.Do(req) diff --git a/http/token_parser.go b/http/tokens.go similarity index 53% rename from http/token_parser.go rename to http/tokens.go index c780e988b2..980bce7b7d 100644 --- a/http/token_parser.go +++ b/http/tokens.go @@ -2,20 +2,21 @@ package http import ( "errors" + "fmt" "net/http" "strings" ) -const tokenScheme = "Token " +const tokenScheme = "Token " // TODO(goller): I'd like this to be Bearer // errors var ( ErrAuthHeaderMissing = errors.New("Authorization Header is missing") - ErrAuthBadScheme = errors.New("Authorization Header Scheme has to Token") + ErrAuthBadScheme = errors.New("Authorization Header Scheme is invalid") ) -// ParseAuthHeaderToken will parse the token from http Authorization Header. -func ParseAuthHeaderToken(r *http.Request) (string, error) { +// GetToken will parse the token from http Authorization Header. +func GetToken(r *http.Request) (string, error) { header := r.Header.Get("Authorization") if header == "" { return "", ErrAuthHeaderMissing @@ -25,3 +26,8 @@ func ParseAuthHeaderToken(r *http.Request) (string, error) { } return header[len(tokenScheme):], nil } + +// SetToken adds the token to the request. +func SetToken(token string, req *http.Request) { + req.Header.Set("Authorization", fmt.Sprintf("%s%s", tokenScheme, token)) +} diff --git a/http/token_parse_test.go b/http/tokens_test.go similarity index 60% rename from http/token_parse_test.go rename to http/tokens_test.go index 3f798c12cb..8b33ac6ece 100644 --- a/http/token_parse_test.go +++ b/http/tokens_test.go @@ -2,10 +2,19 @@ package http import ( "net/http" + "net/http/httptest" "testing" ) -func TestParseAuthHeader(t *testing.T) { +func tokenRequest(token string) *http.Request { + req := httptest.NewRequest("GET", "/", nil) + if token != "" { + SetToken(token, req) + } + return req +} + +func TestGetToken(t *testing.T) { type args struct { header string } @@ -62,7 +71,7 @@ func TestParseAuthHeader(t *testing.T) { Header: make(http.Header), } req.Header.Set("Authorization", tt.args.header) - result, err := ParseAuthHeaderToken(req) + result, err := GetToken(req) if err != tt.wants.err { t.Errorf("err incorrect want %v, got %v", tt.wants.err, err) return @@ -74,3 +83,27 @@ func TestParseAuthHeader(t *testing.T) { } } + +func TestSetToken(t *testing.T) { + tests := []struct { + name string + token string + req *http.Request + want string + }{ + { + name: "adding token to Authorization header", + token: "1234", + req: httptest.NewRequest("GET", "/", nil), + want: "Token 1234", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + SetToken(tt.token, tt.req) + if got := tt.req.Header.Get("Authorization"); got != tt.want { + t.Errorf("SetToken() want %s, got %s", tt.want, got) + } + }) + } +} diff --git a/http/user_service.go b/http/user_service.go index 4445e948e7..6730a883d9 100644 --- a/http/user_service.go +++ b/http/user_service.go @@ -265,7 +265,7 @@ func (s *UserService) FindUserByID(ctx context.Context, id platform.ID) (*platfo if err != nil { return nil, err } - req.Header.Set("Authorization", s.Token) + SetToken(s.Token, req) hc := newClient(url.Scheme, s.InsecureSkipVerify) resp, err := hc.Do(req) @@ -322,7 +322,7 @@ func (s *UserService) FindUsers(ctx context.Context, filter platform.UserFilter, } req.URL.RawQuery = query.Encode() - req.Header.Set("Authorization", s.Token) + SetToken(s.Token, req) hc := newClient(url.Scheme, s.InsecureSkipVerify) resp, err := hc.Do(req) @@ -364,7 +364,7 @@ func (s *UserService) CreateUser(ctx context.Context, u *platform.User) error { } req.Header.Set("Content-Type", "application/json") - req.Header.Set("Authorization", s.Token) + SetToken(s.Token, req) hc := newClient(url.Scheme, s.InsecureSkipVerify) @@ -404,7 +404,7 @@ func (s *UserService) UpdateUser(ctx context.Context, id platform.ID, upd platfo } req.Header.Set("Content-Type", "application/json") - req.Header.Set("Authorization", s.Token) + SetToken(s.Token, req) hc := newClient(url.Scheme, s.InsecureSkipVerify) @@ -437,7 +437,7 @@ func (s *UserService) DeleteUser(ctx context.Context, id platform.ID) error { if err != nil { return err } - req.Header.Set("Authorization", s.Token) + SetToken(s.Token, req) hc := newClient(url.Scheme, s.InsecureSkipVerify) resp, err := hc.Do(req) diff --git a/prometheus/auth_service.go b/prometheus/auth_service.go index fee0bf15c7..93254e83a5 100644 --- a/prometheus/auth_service.go +++ b/prometheus/auth_service.go @@ -109,6 +109,35 @@ func (s *AuthorizationService) DeleteAuthorization(ctx context.Context, id platf return s.AuthorizationService.DeleteAuthorization(ctx, id) } +// DisableAuthorization disables an authorization, records function call latency, and counts function calls. +func (s *AuthorizationService) DisableAuthorization(ctx context.Context, id platform.ID) (err error) { + defer func(start time.Time) { + labels := prometheus.Labels{ + "method": "DisableAuthorization", + "error": fmt.Sprint(err != nil), + } + s.requestCount.With(labels).Add(1) + s.requestDuration.With(labels).Observe(time.Since(start).Seconds()) + }(time.Now()) + + return s.AuthorizationService.DisableAuthorization(ctx, id) +} + +// EnableAuthorization enables an authorization, records function call latency, and counts function calls. +func (s *AuthorizationService) EnableAuthorization(ctx context.Context, id platform.ID) (err error) { + defer func(start time.Time) { + labels := prometheus.Labels{ + "method": "EnableAuthorization", + "error": fmt.Sprint(err != nil), + } + s.requestCount.With(labels).Add(1) + s.requestDuration.With(labels).Observe(time.Since(start).Seconds()) + }(time.Now()) + + return s.AuthorizationService.EnableAuthorization(ctx, id) +} + +// PrometheusCollectors returns all authorization service prometheus collectors. func (s *AuthorizationService) PrometheusCollectors() []prometheus.Collector { return []prometheus.Collector{ s.requestCount, diff --git a/prometheus/auth_service_test.go b/prometheus/auth_service_test.go index 38a6569e28..4221c545a3 100644 --- a/prometheus/auth_service_test.go +++ b/prometheus/auth_service_test.go @@ -38,6 +38,14 @@ func (a *authzSvc) DeleteAuthorization(context.Context, platform.ID) error { return a.Err } +func (a *authzSvc) DisableAuthorization(context.Context, platform.ID) error { + return a.Err +} + +func (a *authzSvc) EnableAuthorization(context.Context, platform.ID) error { + return a.Err +} + func TestAuthorizationService_Metrics(t *testing.T) { a := new(authzSvc) diff --git a/task/validator.go b/task/validator.go index d677aa772d..133b7dfcfe 100644 --- a/task/validator.go +++ b/task/validator.go @@ -47,7 +47,7 @@ func validatePermission(ctx context.Context, perm platform.Permission) error { return err } - if !platform.Allowed(perm, auth.Permissions) { + if !platform.Allowed(perm, auth) { return authError{error: ErrFailedPermission, perm: perm, auth: auth} }