-
Notifications
You must be signed in to change notification settings - Fork 0
Add validate namespace endpoint to m3ctl #50
Conversation
1 similar comment
1 similar comment
service/r2/kv/store.go
Outdated
} | ||
|
||
// 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 { |
There was a problem hiding this comment.
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
?
There was a problem hiding this comment.
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.
service/r2/kv/store.go
Outdated
@@ -73,6 +75,10 @@ func (s *store) FetchNamespaces() (*rules.NamespacesView, error) { | |||
}, nil | |||
} | |||
|
|||
func (s *store) ValidateNamespace(rs *rules.RuleSetSnapshot) error { |
There was a problem hiding this comment.
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?
service/r2/kv/store.go
Outdated
@@ -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) |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
service/r2/routes.go
Outdated
} | ||
if vars[namespaceIDVar] != rsj.Namespace { | ||
return nil, fmt.Errorf( | ||
"NamespaceID param: %s and RuleSet namespaceID: %s do not match", |
There was a problem hiding this comment.
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
?
service/r2/routes.go
Outdated
if err := s.store.ValidateNamespace(rss); err != nil { | ||
return nil, err | ||
} | ||
return fmt.Sprintf("Namespace and rule-set are valid"), nil |
There was a problem hiding this comment.
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
?
service/r2/service.go
Outdated
@@ -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) |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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
}
service/r2/service.go
Outdated
} | ||
|
||
for _, mr := range r.MappingRules { | ||
rss.MappingRules[mr.Name] = mr.mappingRuleView() |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yup, changed.
service/r2/service.go
Outdated
rss.MappingRules[mr.Name] = mr.mappingRuleView() | ||
} | ||
for _, rr := range r.RollupRules { | ||
rss.RollupRules[rr.Name] = rr.rollupRuleView() |
There was a problem hiding this comment.
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?
service/r2/stub/store.go
Outdated
@@ -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 |
There was a problem hiding this comment.
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.
service/r2/store.go
Outdated
@@ -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 |
There was a problem hiding this comment.
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.
service/r2/routes.go
Outdated
) | ||
} | ||
|
||
rss := rsj.ruleSetSnapshot() |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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 :)
services/r2ctl/config/r2ctl.go
Outdated
@@ -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 |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yup, added there
There was a problem hiding this 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.
service/r2/kv/store.go
Outdated
} | ||
|
||
// 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 { |
There was a problem hiding this comment.
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.
service/r2/kv/store.go
Outdated
@@ -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) |
There was a problem hiding this comment.
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.
service/r2/routes.go
Outdated
@@ -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) { |
There was a problem hiding this comment.
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)
service/r2/routes.go
Outdated
) | ||
} | ||
|
||
rss := rsj.ruleSetSnapshot() |
There was a problem hiding this comment.
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 :)
service/r2/service.go
Outdated
} | ||
|
||
for _, mr := range r.MappingRules { | ||
rss.MappingRules[mr.Name] = mr.mappingRuleView() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yup, changed.
services/r2ctl/config/r2ctl.go
Outdated
@@ -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 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yup, added there
1 similar comment
@@ -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 |
There was a problem hiding this comment.
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 ?
There was a problem hiding this comment.
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.
service/r2/routes.go
Outdated
if err := s.store.ValidateRuleSet(rss); err != nil { | ||
return nil, err | ||
} | ||
return fmt.Sprintf("Ruleset is valid"), nil |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
-
I think you can just return a string, no need to use fmt.Sprintf
-
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?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
- Thanks - nice catch.
- Following convention on the other endpoints that return string values in the service (ie deleteMappingRule, deleteRollupRule, etc.) I'm open to whatever either way.
service/r2/service.go
Outdated
@@ -36,6 +36,7 @@ import ( | |||
"github.com/m3db/m3metrics/rules" | |||
"github.com/m3db/m3x/clock" | |||
"github.com/m3db/m3x/instrument" | |||
"github.com/pborman/uuid" |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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.
service/r2/service.go
Outdated
|
||
// 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). |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
service/r2/service.go
Outdated
id = uuid.New() | ||
mr.ID = id | ||
} | ||
rss.MappingRules[id] = mr.mappingRuleView() |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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
There was a problem hiding this 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.
1 similar comment
@@ -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 |
There was a problem hiding this comment.
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.
service/r2/kv/store.go
Outdated
validator := s.opts.Validator() | ||
// No validator is set so by default the ruleSetSnapshot is valid | ||
if validator == nil { | ||
return nil |
There was a problem hiding this comment.
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?
service/r2/service.go
Outdated
@@ -36,6 +36,7 @@ import ( | |||
"github.com/m3db/m3metrics/rules" | |||
"github.com/m3db/m3x/clock" | |||
"github.com/m3db/m3x/instrument" | |||
"github.com/pborman/uuid" |
There was a problem hiding this comment.
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.
service/r2/service.go
Outdated
@@ -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) |
There was a problem hiding this comment.
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
}
service/r2/service.go
Outdated
|
||
// 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). |
There was a problem hiding this comment.
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.
service/r2/service.go
Outdated
rss := rules.RuleSetSnapshot{ | ||
Namespace: r.Namespace, | ||
Version: r.Version, | ||
MappingRules: map[string]*rules.MappingRuleView{}, |
There was a problem hiding this comment.
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,
}
service/r2/service_test.go
Outdated
import ( | ||
"testing" | ||
|
||
"github.com/pborman/uuid" |
There was a problem hiding this comment.
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. |
There was a problem hiding this comment.
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! 👍
1 similar comment
auth/simple.go
Outdated
return a.authorizeUser(a.readWhitelistEnabled, a.readWhitelistedUserIDs, userID) | ||
case http.MethodPost, http.MethodPut, http.MethodPatch, http.MethodDelete: | ||
case AuthorizationTypeWriteOnly: |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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.
Looks like there is a significant overhaul to how auth works. Can you give a quick rundown on why you made the changes? |
service/r2/kv/store.go
Outdated
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") |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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")
@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 |
There was a problem hiding this comment.
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
?
There was a problem hiding this comment.
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:
- Changing them to None/ReadOnly/etc reserves those names for the entire auth package
- 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
There was a problem hiding this comment.
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 { |
There was a problem hiding this comment.
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) |
There was a problem hiding this comment.
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) |
There was a problem hiding this comment.
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) |
There was a problem hiding this comment.
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.
service/r2/service.go
Outdated
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) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Same question as above.
service/r2/service.go
Outdated
@@ -553,3 +600,49 @@ type ruleSetJSON struct { | |||
MappingRules []mappingRuleJSON `json:"mappingRules"` | |||
RollupRules []rollupRuleJSON `json:"rollupRules"` | |||
} | |||
|
|||
type genID int |
There was a problem hiding this comment.
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.
service/r2/service.go
Outdated
type genID int | ||
|
||
const ( | ||
genIDFalse genID = iota |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
generateID
service/r2/service.go
Outdated
|
||
const ( | ||
genIDFalse genID = iota | ||
genIDTrue |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
dontGenerateID
.
service/r2/service.go
Outdated
// 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) { | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Remove blank line.
1 similar comment
service/r2/convert.go
Outdated
type idGenType int | ||
|
||
const ( | ||
generateID idGenType = iota |
There was a problem hiding this comment.
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 { |
There was a problem hiding this comment.
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?
service/r2/service.go
Outdated
if !exists { | ||
var err error | ||
if authType = defaultAuthorizationTypeForHTTPMethod(method); authType == auth.AuthorizationTypeUnknown { | ||
return err |
There was a problem hiding this comment.
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?
auth/simple.go
Outdated
} | ||
} | ||
|
||
func (a simpleAuthorization) authorizeUserForRead(userID string) error { |
There was a problem hiding this comment.
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.
service/r2/convert.go
Outdated
"github.com/m3db/m3metrics/policy" | ||
"github.com/m3db/m3metrics/rules" | ||
"github.com/pborman/uuid" |
There was a problem hiding this comment.
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.
service/r2/convert.go
Outdated
@@ -195,3 +198,49 @@ func newRuleSetJSON(latest *rules.RuleSetSnapshot) ruleSetJSON { | |||
RollupRules: rrJSON, | |||
} | |||
} | |||
|
|||
// TODO(@SANDUSKY) MOVE THIS |
There was a problem hiding this comment.
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" |
There was a problem hiding this comment.
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") |
There was a problem hiding this comment.
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".
service/r2/kv/store.go
Outdated
@@ -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 |
There was a problem hiding this comment.
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.
service/r2/service.go
Outdated
@@ -96,6 +99,39 @@ func newServiceMetrics(scope tally.Scope, samplingRate float64) serviceMetrics { | |||
} | |||
} | |||
|
|||
type route struct { |
There was a problem hiding this comment.
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.
service/r2/service.go
Outdated
{path: validateRuleSetPath, method: http.MethodPost}: auth.AuthorizationTypeReadOnly, | ||
} | ||
|
||
func defaultAuthorizationTypeForHTTPMethod(method string) auth.AuthorizationType { |
There was a problem hiding this comment.
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
.
services/r2ctl/main/main.go
Outdated
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) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
super nit: initialize
-> create
.
service/r2/service.go
Outdated
// 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 |
There was a problem hiding this comment.
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
1 similar comment
auth/simple.go
Outdated
} | ||
|
||
for _, u := range a.readWhitelistedUserIDs { | ||
func authorizeUserForAccess(userID string, whitelistedUserIDs []string) error { |
There was a problem hiding this comment.
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.
service/r2/service.go
Outdated
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) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
swap method
and path
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for the catch.
LGTM, good stuff! Might wanna wait for travis though. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LGTM
This PR adds a namespace validation endpoint to m3ctl. Will probably add a couple more tests before merging.