Skip to content
This repository has been archived by the owner on Feb 28, 2019. It is now read-only.

Add validate namespace endpoint to m3ctl #50

Merged
merged 15 commits into from
Jan 12, 2018
Merged

Conversation

m-sandusky
Copy link
Contributor

This PR adds a namespace validation endpoint to m3ctl. Will probably add a couple more tests before merging.

@coveralls
Copy link

Coverage Status

Coverage decreased (-0.06%) to 52.727% when pulling 136e0a5 on add-validation-endpoint into 706b2dd on master.

1 similar comment
@coveralls
Copy link

Coverage Status

Coverage decreased (-0.06%) to 52.727% when pulling 136e0a5 on add-validation-endpoint into 706b2dd on master.

@coveralls
Copy link

Coverage Status

Coverage decreased (-0.4%) to 52.42% when pulling e4dd37f on add-validation-endpoint into 706b2dd on master.

1 similar comment
@coveralls
Copy link

Coverage Status

Coverage decreased (-0.4%) to 52.42% when pulling e4dd37f on add-validation-endpoint into 706b2dd on master.

}

// NewStore returns a new service that knows how to talk to a kv backed r2 store.
func NewStore(rs rules.Store, opts StoreOptions) r2.Store {
func NewStore(rs rules.Store, v rules.Validator, opts StoreOptions) r2.Store {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Perhaps make validator part of the StoreOptions?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I thought about this, and to me if we're going to throw an error on the validator no being present (from the below comment) then it would make sense to have the Validator be a required param.

However, if passing a Validator should be optional then the endpoint should have a successful validation for every request if no validator was configured for the store. I've added the logic for this but can change it back.

@@ -73,6 +75,10 @@ func (s *store) FetchNamespaces() (*rules.NamespacesView, error) {
}, nil
}

func (s *store) ValidateNamespace(rs *rules.RuleSetSnapshot) error {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Perhaps call this ValidateRuleSet to be more accurate?

@@ -73,6 +75,10 @@ func (s *store) FetchNamespaces() (*rules.NamespacesView, error) {
}, nil
}

func (s *store) ValidateNamespace(rs *rules.RuleSetSnapshot) error {
return s.validator.ValidateSnapshot(rs)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd check if s.validator is nil and return an errNilValidator = errors.New("validator not provided") just to be defensive.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

would it be reasonable to panic on NewStore if no validator is passed in? We have a stub validator so even for tests there should always be one available.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's interesting - essentially we want the validator that is used in the instance of kv.TxnStore in the construction of NewStore to be the same. We allow for a nil validator to be passed in there (and bypass any validation if so) so if that's the case I'd assume everything passed to the validation endpoint should succeed.

}
if vars[namespaceIDVar] != rsj.Namespace {
return nil, fmt.Errorf(
"NamespaceID param: %s and RuleSet namespaceID: %s do not match",
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: error messages should start with lowercase letters. Also might be easier on the eyes to say
namespaceID param %s and ruleset namespaceID %s do not match?

if err := s.store.ValidateNamespace(rss); err != nil {
return nil, err
}
return fmt.Sprintf("Namespace and rule-set are valid"), nil
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Perhaps just return Ruleset is valid?

@@ -192,6 +193,7 @@ func (s *service) RegisterHandlers(router *mux.Router) {

router.Handle(namespacePath, h.wrap(s.fetchNamespaces)).Methods(http.MethodGet)
router.Handle(namespacePath, h.wrap(s.createNamespace)).Methods(http.MethodPost)
router.Handle(namespaceValidatePath, h.wrap(s.validateNamespace)).Methods(http.MethodPost)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How is auth going to work for validation endpoint? Is it "if a user has read access, then he/she can validate?" Or does he/she need to have write access to validate?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Left a comment on this earlier - not sure why it didn't post. In any case this is a good point. If the requester does not have write access, due to how the current auth is set up, they will not be validated for the endpoint. The way around this is to make the endpoint GET or to change how we do auth.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we can make the NewAuthHandler take an additional parameter authType:

type authorizationType int

const (
  none authorizationType
  readOnly
  writeOnly
  readWrite
)

func (a simpleAuth) NewAuthHandler(
  authType authorizationType,
  next http.Handler,
  errHandler errorResponseHandler,
) http.Handler {
  ...
}

return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
  ...
  switch authType {
    case none:
      ...
    case readOnly:
      ...
    case writeOnly:
      ...
    case readWrite:
      ...
}

Then in service.go, register the auth type

func registerRoute(router *mux.Router, path, method string, handler http.Handler) error {
  authType, exists := authRegistry[endpoint{Path:path, Method: method}]
  if !exists {
    // Should make this a method instead.
    switch method:
    case http.GET:
      authType = readOnly
    case http.POST, ...:
      authType = writeOnly
    default:
      return errUnknownHTTPMethod
  }
  fn := h.wrap(authType, handler)
  router.Handle(path, fn).Methods(method)
  return nil
}

func (h r2Handler) wrap(authType authorizationType, fn r2HandlerFunc) http.Handler {
	f := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		if err := fn(w, r); err != nil {
			h.handleError(w, err)
		}
	})
	return h.auth.NewAuthHandler(authType, f, writeAPIResponse)
}

type endpoint struct {
  Path string
  Method string
}

var authRegistry = map[endpoint]authorizationType{
  {Path: namespaceValidePath, Method: http.POST}: readOnly
}

}

for _, mr := range r.MappingRules {
rss.MappingRules[mr.Name] = mr.mappingRuleView()
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hm we should use the UUID rather than the name no?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yup, changed.

rss.MappingRules[mr.Name] = mr.mappingRuleView()
}
for _, rr := range r.RollupRules {
rss.RollupRules[rr.Name] = rr.rollupRuleView()
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hm we should use the UUID rather than the name no?

@@ -304,6 +304,11 @@ func (s *store) CreateNamespace(namespaceID string, uOpts r2.UpdateOptions) (*ru
}
}

func (s *store) ValidateNamespace(rs *rules.RuleSetSnapshot) error {
// Assumes no validation config for stub store so all rule sets are valid
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: add a period at the end.

@@ -27,6 +27,9 @@ type Store interface {
// FetchNamespaces fetches namespaces.
FetchNamespaces() (*rules.NamespacesView, error)

// ValidateNamespace validates a namespace's ruleset.
ValidateNamespace(rs *rules.RuleSetSnapshot) error
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ValidateRuleSet is probably more accurate in this case.

)
}

rss := rsj.ruleSetSnapshot()
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

NIT:
Given that this function is only called in this file, and ruleSetSnapshot would be the only stuct with methods on it. Perhaps make this function take a ruleSetJSON and return a ruleSetSnapshot. The It might also have it as sort of public util function.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was following the general convention with converting the JSON structs to View/Snapshots (for better or for worse), open to change if you feel strongly about it :)

@@ -131,5 +131,5 @@ func (c kvStoreConfig) NewStore(instrumentOpts instrument.Options) (r2.Store, er
r2StoreOpts := r2kv.NewStoreOptions().
SetInstrumentOptions(instrumentOpts).
SetRuleUpdatePropagationDelay(c.PropagationDelay)
return r2kv.NewStore(rulesStore, r2StoreOpts), nil
return r2kv.NewStore(rulesStore, validator, r2StoreOpts), nil
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I dont know enough to say either way, but could the validator go on the r2StoreOptions? If we already have a catch-all for parameters to this function perhaps we should just add it there instead.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yup, added there

Copy link
Contributor Author

@m-sandusky m-sandusky left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@xichen2020 @jskelcy Thanks for the review! I've addressed your comments.

}

// NewStore returns a new service that knows how to talk to a kv backed r2 store.
func NewStore(rs rules.Store, opts StoreOptions) r2.Store {
func NewStore(rs rules.Store, v rules.Validator, opts StoreOptions) r2.Store {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I thought about this, and to me if we're going to throw an error on the validator no being present (from the below comment) then it would make sense to have the Validator be a required param.

However, if passing a Validator should be optional then the endpoint should have a successful validation for every request if no validator was configured for the store. I've added the logic for this but can change it back.

@@ -73,6 +75,10 @@ func (s *store) FetchNamespaces() (*rules.NamespacesView, error) {
}, nil
}

func (s *store) ValidateNamespace(rs *rules.RuleSetSnapshot) error {
return s.validator.ValidateSnapshot(rs)
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's interesting - essentially we want the validator that is used in the instance of kv.TxnStore in the construction of NewStore to be the same. We allow for a nil validator to be passed in there (and bypass any validation if so) so if that's the case I'd assume everything passed to the validation endpoint should succeed.

@@ -64,6 +64,27 @@ func createNamespace(s *service, r *http.Request) (data interface{}, err error)
return newNamespaceJSON(view), nil
}

func validateNamespace(s *service, r *http.Request) (data interface{}, err error) {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we edit the actual endpoint to reflect this as well? ie (/namespaces/{namespace_id}/validate_rule_set)

)
}

rss := rsj.ruleSetSnapshot()
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was following the general convention with converting the JSON structs to View/Snapshots (for better or for worse), open to change if you feel strongly about it :)

}

for _, mr := range r.MappingRules {
rss.MappingRules[mr.Name] = mr.mappingRuleView()
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yup, changed.

@@ -131,5 +131,5 @@ func (c kvStoreConfig) NewStore(instrumentOpts instrument.Options) (r2.Store, er
r2StoreOpts := r2kv.NewStoreOptions().
SetInstrumentOptions(instrumentOpts).
SetRuleUpdatePropagationDelay(c.PropagationDelay)
return r2kv.NewStore(rulesStore, r2StoreOpts), nil
return r2kv.NewStore(rulesStore, validator, r2StoreOpts), nil
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yup, added there

@coveralls
Copy link

Coverage Status

Coverage increased (+4.0%) to 56.772% when pulling 658b6a7 on add-validation-endpoint into 706b2dd on master.

1 similar comment
@coveralls
Copy link

Coverage Status

Coverage increased (+4.0%) to 56.772% when pulling 658b6a7 on add-validation-endpoint into 706b2dd on master.

@@ -96,3 +104,13 @@ func (o *storeOptions) SetRuleUpdatePropagationDelay(value time.Duration) StoreO
func (o *storeOptions) RuleUpdatePropagationDelay() time.Duration {
return o.ruleUpdatePropagationDelay
}

func (o *storeOptions) SetValidator(value rules.Validator) StoreOptions {
opts := *o
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This appears to be the convention but is there a need for this deref @xichen2020 ?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah so this is to ensure the options are immutable.

if err := s.store.ValidateRuleSet(rss); err != nil {
return nil, err
}
return fmt.Sprintf("Ruleset is valid"), nil
Copy link
Contributor

@jskelcy jskelcy Jan 8, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  1. I think you can just return a string, no need to use fmt.Sprintf

  2. Given that this just returns a string that tells the caller that seems a little self-explanatory do you think this function could just return an error?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  1. Thanks - nice catch.
  2. Following convention on the other endpoints that return string values in the service (ie deleteMappingRule, deleteRollupRule, etc.) I'm open to whatever either way.

@@ -36,6 +36,7 @@ import (
"github.com/m3db/m3metrics/rules"
"github.com/m3db/m3x/clock"
"github.com/m3db/m3x/instrument"
"github.com/pborman/uuid"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Im double checking on this for OSS but I think we may want to use github.com/satori/go.uuid

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, I've used this on in the past as well. m3ctl uses "github.com/pborman/uuid" in other places though so I didn't want add a new dependency for a similar use case. @xichen2020 Do you have any thoughts?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fine w/ either, as long as there's only one for the repo.

Also, please add a blank line between m3db imports and 3rd party imports.


// Creates a new RuleSetSnapshot from a rulesetJSON. If the ruleSetJSON has no IDs for any of its
// mapping rules or rollup rules, it generates missing IDs and sets as a string UUID string so they
// can be stored in a mapping (id -> rule).
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If a rule is being added, not updated is that when there would not be an ID for a rule? I might update the comment to explain what the scenario is where this happens.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah that's exactly it - I'll add a comment for this scenario.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: Change the comment to ruleSetSnapshot creates a ....

You might also make this behavior controlled by an enum passed in though (e.g., genIDType), e.g., if this is an update request, we probably shouldn't generate the missing IDs since that should be a validation error.

@coveralls
Copy link

coveralls commented Jan 8, 2018

Coverage Status

Coverage increased (+4.0%) to 56.772% when pulling 3965184 on add-validation-endpoint into 706b2dd on master.

id = uuid.New()
mr.ID = id
}
rss.MappingRules[id] = mr.mappingRuleView()
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If the mapping id is an empty string and opts.generateMissingID is false this will continuously overwrite rss.MappingRules[""]. Im not sure what the best behavior is, maybe just leave it out of the list? But I think just taking whatever the last rule is and adding that to the snapshot while dropping all the other ones is kind of confusing.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Updated per in person conversation

Copy link
Contributor

@jskelcy jskelcy left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

address the empty string rule id issue.

@coveralls
Copy link

Coverage Status

Coverage increased (+4.0%) to 56.772% when pulling abc04c3 on add-validation-endpoint into 706b2dd on master.

1 similar comment
@coveralls
Copy link

Coverage Status

Coverage increased (+4.0%) to 56.772% when pulling abc04c3 on add-validation-endpoint into 706b2dd on master.

@@ -96,3 +104,13 @@ func (o *storeOptions) SetRuleUpdatePropagationDelay(value time.Duration) StoreO
func (o *storeOptions) RuleUpdatePropagationDelay() time.Duration {
return o.ruleUpdatePropagationDelay
}

func (o *storeOptions) SetValidator(value rules.Validator) StoreOptions {
opts := *o
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah so this is to ensure the options are immutable.

validator := s.opts.Validator()
// No validator is set so by default the ruleSetSnapshot is valid
if validator == nil {
return nil
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Perhaps we should return an error here instead? If we accidentally forget setting the validator in the options, and someone calls the validation endpoint, we should return an error saying the validator is not set instead of returning ok?

@@ -36,6 +36,7 @@ import (
"github.com/m3db/m3metrics/rules"
"github.com/m3db/m3x/clock"
"github.com/m3db/m3x/instrument"
"github.com/pborman/uuid"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fine w/ either, as long as there's only one for the repo.

Also, please add a blank line between m3db imports and 3rd party imports.

@@ -192,6 +193,7 @@ func (s *service) RegisterHandlers(router *mux.Router) {

router.Handle(namespacePath, h.wrap(s.fetchNamespaces)).Methods(http.MethodGet)
router.Handle(namespacePath, h.wrap(s.createNamespace)).Methods(http.MethodPost)
router.Handle(namespaceValidatePath, h.wrap(s.validateNamespace)).Methods(http.MethodPost)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we can make the NewAuthHandler take an additional parameter authType:

type authorizationType int

const (
  none authorizationType
  readOnly
  writeOnly
  readWrite
)

func (a simpleAuth) NewAuthHandler(
  authType authorizationType,
  next http.Handler,
  errHandler errorResponseHandler,
) http.Handler {
  ...
}

return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
  ...
  switch authType {
    case none:
      ...
    case readOnly:
      ...
    case writeOnly:
      ...
    case readWrite:
      ...
}

Then in service.go, register the auth type

func registerRoute(router *mux.Router, path, method string, handler http.Handler) error {
  authType, exists := authRegistry[endpoint{Path:path, Method: method}]
  if !exists {
    // Should make this a method instead.
    switch method:
    case http.GET:
      authType = readOnly
    case http.POST, ...:
      authType = writeOnly
    default:
      return errUnknownHTTPMethod
  }
  fn := h.wrap(authType, handler)
  router.Handle(path, fn).Methods(method)
  return nil
}

func (h r2Handler) wrap(authType authorizationType, fn r2HandlerFunc) http.Handler {
	f := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		if err := fn(w, r); err != nil {
			h.handleError(w, err)
		}
	})
	return h.auth.NewAuthHandler(authType, f, writeAPIResponse)
}

type endpoint struct {
  Path string
  Method string
}

var authRegistry = map[endpoint]authorizationType{
  {Path: namespaceValidePath, Method: http.POST}: readOnly
}


// Creates a new RuleSetSnapshot from a rulesetJSON. If the ruleSetJSON has no IDs for any of its
// mapping rules or rollup rules, it generates missing IDs and sets as a string UUID string so they
// can be stored in a mapping (id -> rule).
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: Change the comment to ruleSetSnapshot creates a ....

You might also make this behavior controlled by an enum passed in though (e.g., genIDType), e.g., if this is an update request, we probably shouldn't generate the missing IDs since that should be a validation error.

rss := rules.RuleSetSnapshot{
Namespace: r.Namespace,
Version: r.Version,
MappingRules: map[string]*rules.MappingRuleView{},
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: it's more efficient to do the following to avoid repeated map reallocations.

mappingRules := make(map[string]*rules.MappingRuleView, len(r.MappingRules))
for _, mr := r.MappingRules {
  ...
}

rollupRules := make(map[string]*rules.RollupRuleView, len(r.RollupRules))
for _, rr := range r.RollupRules {
  ..
}
return rules.RuleSetSnapshot{
  Namespace: r.Namespace,
  Version: r.Version,
  MappingRules: mappingRules,
  RollupRules: rollupRules,
}

import (
"testing"

"github.com/pborman/uuid"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reorder the imports.

@@ -0,0 +1,481 @@
// Copyright (c) 2017 Uber Technologies, Inc.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Brilliant, thanks for the tests! 👍

@coveralls
Copy link

Coverage Status

Coverage increased (+4.2%) to 56.993% when pulling be98154 on add-validation-endpoint into 706b2dd on master.

1 similar comment
@coveralls
Copy link

Coverage Status

Coverage increased (+4.2%) to 56.993% when pulling be98154 on add-validation-endpoint into 706b2dd on master.

auth/simple.go Outdated
return a.authorizeUser(a.readWhitelistEnabled, a.readWhitelistedUserIDs, userID)
case http.MethodPost, http.MethodPut, http.MethodPatch, http.MethodDelete:
case AuthorizationTypeWriteOnly:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What is a write-only user? Who would fall into this bucket?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Write only users would be those that are say whitelisted to make writes but not whitelisted for reads. I can't really think of a current situation where that would be the case but theoretically it could happen (ie a service that writes data potentially sensitive and doesn't have permissions to access any of the read endpoints). I realize that this doesn't really fit our current uses but this is why it was included.

@jskelcy
Copy link
Contributor

jskelcy commented Jan 10, 2018

Looks like there is a significant overhaul to how auth works. Can you give a quick rundown on why you made the changes?

validator := s.opts.Validator()
// If no validator is set, then the validation functionality is not applicable
if validator == nil {
return errors.New("no validator set on StoreOptions so validation is not applicable")
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You can use fmt.Errorf here dont need to mess with imports.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The general rule of thumb is actually to only use fmt.Errorf when you need to format some values as part of the error message. For errors that are just a const string, errors.New is preferred.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: make this a errNilValidator = errors.New("no validator set on StoreOptions so validation is not applicable")

@coveralls
Copy link

coveralls commented Jan 11, 2018

Coverage Status

Coverage increased (+4.6%) to 57.4% when pulling c25d9fe on add-validation-endpoint into 706b2dd on master.

@m-sandusky
Copy link
Contributor Author

@jskelcy In reference to explanation for the auth revamp:

This was done because previously, the Auth handler would authorize users based on HTTP verb sent by the request. Ie In order to be authorized for a POST request, you needed to be a whiltelisted writer and for GET requests, you needed to be a whitelisted reader. The validation endpoint brought up the situation where it is a POST request but you really shouldn't need write whitelist credentials to access it. Xi and I discussed that certain endpoints can have different needs for auth which is where this notion of authorization type comes into play. Each endpoint has an authorization type which is configured when registering the routes for the service (with defaults falling back to GET -> need read access, POST/PUT/DELETE/PATCH need write credentials).

auth/auth.go Outdated
type errorResponseHandler func(w http.ResponseWriter, code int, msg string) error

const (
// UserIDField is a key
UserIDField keyType = iota
)

const (
// AuthorizationTypeNone is the no authorizationType case.
AuthorizationTypeNone AuthorizationType = iota
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: These are a bit of verbose, perhaps just None, ReadOnly, WriteOnly, and ReadWrite?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed that they're verbose but I'd argue for them based on clarity for a couple reasons:

  1. Changing them to None/ReadOnly/etc reserves those names for the entire auth package
  2. Due to the way Go does (or rather doesn't do) enums it's not clear that something like None or ReadOnly would be associated with an AuthorizationType

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's fair, in that case perhaps call them UnknownAuthorization, NoAuthorization, ReadOnlyAuthorization, etc.?

auth/noop.go Outdated
@@ -37,7 +37,7 @@ func NewNoopAuth() HTTPAuthService {
return noopAuth{}
}

func (a noopAuth) NewAuthHandler(next http.Handler, errHandler errorResponseHandler) http.Handler {
func (a noopAuth) NewAuthHandler(authType AuthorizationType, next http.Handler, errHandler errorResponseHandler) http.Handler {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: omit the parameter name authType or use _ since it's not used here.

auth/simple.go Outdated
switch authType {
case AuthorizationTypeNone:
return nil
case AuthorizationTypeReadOnly:
return a.authorizeUser(a.readWhitelistEnabled, a.readWhitelistedUserIDs, userID)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: This can be moved to a a.authorizeUserForRead method

auth/simple.go Outdated
return a.authorizeUser(a.readWhitelistEnabled, a.readWhitelistedUserIDs, userID)
case http.MethodPost, http.MethodPut, http.MethodPatch, http.MethodDelete:
case AuthorizationTypeWriteOnly:
return a.authorizeUser(a.writeWhitelistEnabled, a.writeWhitelistedUserIDs, userID)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: This can be moved to a a.authorizeUserForWrite method.

auth/simple.go Outdated
case AuthorizationTypeWriteOnly:
return a.authorizeUser(a.writeWhitelistEnabled, a.writeWhitelistedUserIDs, userID)
case AuthorizationTypeReadWrite:
err := a.authorizeUser(a.readWhitelistEnabled, a.readWhitelistedUserIDs, userID)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: This can reusea.authorizeUserForRead and a.authorizeUserForWrite methods defined above.

router.Handle(rollupRuleWithIDPath, h.wrap(s.updateRollupRule)).Methods(http.MethodPut, http.MethodPatch)
router.Handle(rollupRuleWithIDPath, h.wrap(s.deleteRollupRule)).Methods(http.MethodDelete)
registerRoute(router, rollupRuleWithIDPath, http.MethodGet, h, s.fetchRollupRule)
registerRoute(router, rollupRuleWithIDPath, http.MethodPut, h, s.updateRollupRule)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same question as above.

@@ -553,3 +600,49 @@ type ruleSetJSON struct {
MappingRules []mappingRuleJSON `json:"mappingRules"`
RollupRules []rollupRuleJSON `json:"rollupRules"`
}

type genID int
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: idGenType might be better.

type genID int

const (
genIDFalse genID = iota
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

generateID


const (
genIDFalse genID = iota
genIDTrue
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

dontGenerateID.

// for any of its mapping rules or rollup rules, it generates missing IDs and sets as a string UUID
// string so they can be stored in a mapping (id -> rule).
func (r ruleSetJSON) ruleSetSnapshot(genID genID) (*rules.RuleSetSnapshot, error) {

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Remove blank line.

@coveralls
Copy link

Coverage Status

Coverage increased (+2.6%) to 55.832% when pulling c4c62a8 on add-validation-endpoint into 98073fe on master.

1 similar comment
@coveralls
Copy link

Coverage Status

Coverage increased (+2.6%) to 55.832% when pulling c4c62a8 on add-validation-endpoint into 98073fe on master.

type idGenType int

const (
generateID idGenType = iota
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

NIT: Given that this is a flag, and probably always will be, this name is a little misleading. Making it sound like there could be more than 2 values.

@@ -61,10 +61,11 @@ func (s *service) URLPrefix() string {
return healthURL
}

func (s *service) RegisterHandlers(router *mux.Router) {
func (s *service) RegisterHandlers(router *mux.Router) error {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What does this function need to return something now?

if !exists {
var err error
if authType = defaultAuthorizationTypeForHTTPMethod(method); authType == auth.AuthorizationTypeUnknown {
return err
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

are we returning an empty error here?

@coveralls
Copy link

coveralls commented Jan 11, 2018

Coverage Status

Coverage increased (+2.7%) to 55.906% when pulling 4e8d3a2 on add-validation-endpoint into 98073fe on master.

auth/simple.go Outdated
}
}

func (a simpleAuthorization) authorizeUserForRead(userID string) error {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we pull the common code from authorizeUserForRead and authorizeUserForWrite into a common authorizeUserForAccess function and reuse it? The authorizeUserForAccess implementation can just be the same as the old authorizeUser function.

"github.com/m3db/m3metrics/policy"
"github.com/m3db/m3metrics/rules"
"github.com/pborman/uuid"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Blank line between m3db imports and 3rd party imports.

@@ -195,3 +198,49 @@ func newRuleSetJSON(latest *rules.RuleSetSnapshot) ruleSetJSON {
RollupRules: rrJSON,
}
}

// TODO(@SANDUSKY) MOVE THIS
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this TODO still necessary?

"testing"

"github.com/m3db/m3metrics/policy"
"github.com/m3db/m3metrics/rules"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Blank line between m3db imports and 3rd party imports.

@@ -37,6 +38,8 @@ type store struct {
updateHelper rules.RuleSetUpdateHelper
}

var errNilValidator = errors.New("no validator set on StoreOptions so validation is not applicable")
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

super nit: might read a little better to say "no validator set so validation is not applicable".

@@ -73,6 +76,15 @@ func (s *store) FetchNamespaces() (*rules.NamespacesView, error) {
}, nil
}

func (s *store) ValidateRuleSet(rs *rules.RuleSetSnapshot) error {
validator := s.opts.Validator()
// If no validator is set, then the validation functionality is not applicable
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Comments should end w/ a period.

@@ -96,6 +99,39 @@ func newServiceMetrics(scope tally.Scope, samplingRate float64) serviceMetrics {
}
}

type route struct {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

super nit: might wanna consider moving route to the bottom so the service related pieces are in one piece.

{path: validateRuleSetPath, method: http.MethodPost}: auth.AuthorizationTypeReadOnly,
}

func defaultAuthorizationTypeForHTTPMethod(method string) auth.AuthorizationType {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd still return an error here just to make it very explicit that if we don't see one of the expected methods, it should be an error, and check error in registerRoute.

server := http.NewServer(listenAddr, httpServerOpts, r2Service, healthService)
server, err := http.NewServer(listenAddr, httpServerOpts, r2Service, healthService)
if err != nil {
logger.Fatalf("could not initialize new server: %v", err)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

super nit: initialize -> create.

// Namespaces action
h := r2Handler{s.logger, s.authService}

router.Handle(namespacePath, h.wrap(s.fetchNamespaces)).Methods(http.MethodGet)
router.Handle(namespacePath, h.wrap(s.createNamespace)).Methods(http.MethodPost)
// Namespaces actions
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Might be more cleaner to do:

// Register routes.
routeWithHandlers := []struct{
   route route
   handler r2HandlerFnc
} {
  {route: {path: namespacePath: method: http.MethodGet}, handler: s.fetchNamespaces},
  {route: {path: namespacePath: method: http.MethodPost}, handler: s.createNamespace},
  ...
}
for _, rh := range routeWithHandlers {
  if err := registerRoute(router, rh.route.path, rh.route.method, h, rh.handler); err != nil {
    return err
  }
}
return nil

@coveralls
Copy link

Coverage Status

Coverage increased (+3.8%) to 56.991% when pulling e94056e on add-validation-endpoint into 98073fe on master.

1 similar comment
@coveralls
Copy link

Coverage Status

Coverage increased (+3.8%) to 56.991% when pulling e94056e on add-validation-endpoint into 98073fe on master.

auth/simple.go Outdated
}

for _, u := range a.readWhitelistedUserIDs {
func authorizeUserForAccess(userID string, whitelistedUserIDs []string) error {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: could take a boolean parameter enabled and further extract the enabled check in the read and write methods below.

return fmt.Errorf("unknown authorization type for method %s at path %s", method, path)
var err error
if authType, err = defaultAuthorizationTypeForHTTPMethod(method); err != nil {
return fmt.Errorf("could not register route for path %s and method %s, error: %v", method, path, err)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

swap method and path?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the catch.

@xichen2020
Copy link
Contributor

LGTM, good stuff!

Might wanna wait for travis though.

Copy link
Contributor

@xichen2020 xichen2020 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

@coveralls
Copy link

Coverage Status

Coverage increased (+4.1%) to 57.333% when pulling 88cc790 on add-validation-endpoint into 98073fe on master.

1 similar comment
@coveralls
Copy link

Coverage Status

Coverage increased (+4.1%) to 57.333% when pulling 88cc790 on add-validation-endpoint into 98073fe on master.

@m-sandusky m-sandusky merged commit 09cd37e into master Jan 12, 2018
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants