-
Notifications
You must be signed in to change notification settings - Fork 525
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add apikey subcommand #3063
Add apikey subcommand #3063
Conversation
1c9d1bb
to
3214f42
Compare
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.
A couple of remarks after playing around with the commands:
- expiration information misleading: when returning an invalidated API Key
Expiration ..... never
is misleading - output consistency: there is if no
expiration
date is set - noexpiration
key injson
output butExpiration...... never
in text output - require parameter for invalidate:
./apm-server apikey invalidate
throws an ES error. Requireid
orname
as parameter and throw more user friendly error if missing - require parameter for verify:
./apm-server apikey verify --credentials
make credentials a required parameter instead of a flag as it needs to be passed in for a successful request - consistency when creating privileges: Why does
./apm-server apikey create --ingest
create all security privileges and not only foringest
? - verifying non-existing privileges:
./apm-server apikey verify --credentials XXX --ingest --something
does not print anything. Can you add some output along the lines of non-existing privilege, or if this information is not available an output that the key is invalid for it?
Some bugs:
- name flag does not seem to be picked up when invalidating keys:
./apm-server apikey invalidate --name apm-systemtest
throws exception, while./apm-server apikey invalidate --id 90Zm_m4BXuxExeOtJ1ki
works just fine with the same seetings.
Raised error:
Error invalidating API Key(s), do you have the "manage_cluster" security privilege?
{"error":{"root_cause":[{"type":"action_request_validation_exception","reason":"Validation Failed: 1: One of [api key id, api key name, username, realm name] must be specified if [owner] flag is false;"}],"type":"action_request_validation_exception","reason":"Validation Failed: 1: One of [api key id, api key name, username, realm name] must be specified if [owner] flag is false;"},"status":400}
-
verify only verifies first privilege:
./apm-server apikey create --ingest --agent-config
, followed by./apm-server apikey verify --credentials XXX --ingest --sourcemap --agent-config
returnsAuthorized for privilege "config_agent:read"...: No
, but when only verifyingagent-config
the output returnsYes
I suspect that only the first passed in flag (e.g.--ingest
) is used for the query -
name flag does not seem to be picked up when info is provided:
./apm-server apikey info --name apm-key
provided name is ignored and all keys returned
Will provide code review separately when done.
Thanks. A few of those comments unfortunately are related to I expect this subcommand to be used programmatically, so I think is important that it always honours the I fixed however some pointers, which should help with the
I think is OK that output is different data for human and machine consumption. A "never" string is going to be of no use for a program, so it mimics the Elasticsearch response (no
Because the user doesn't care, and the code is simpler that way (with less conditionals). Maintaining it is going to be a bit more complicated (https://github.com/elastic/apm-server/pull/3063/files#diff-2d9068aff6ea838c5c2463d1fd9bee36R40-R42), so I think is important to keep it simple.
Good catch. It turns out that we can't reuse the same
This is an Elasticsearch thing, "expired" is not the same as "invalidated", and it can happen that either is |
) | ||
|
||
// Client is an interface designed to abstract away version differences between elasticsearch clients | ||
type Client interface { | ||
// TODO: deprecate |
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 exactly should be deprecated and why?
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 line bellow it, the Search
interface. Mostly because it is redundant now:
it can be implemented with the JsonRequest
and richer data types (as functions in security_api.go
, instead of requiring and returning low level readers.
It would also resolve colliding names, which would allow us to embed the whole client struct in the wrapper, avoiding the need for more (confusing) names: #3063 (comment)
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 you planning on doing this within this PR? If not, please create an issue with a small description so someone can pick it up and maybe link it here to the TODO.
I assumed that Regarding the issues you are facing when using cobra and the
|
I am pretty sure the issue was somewhere else and resolved by one of the other fixes. Having the builder outside the loop works just fine, please change it again. |
@@ -17,11 +17,13 @@ | |||
|
|||
package authorization | |||
|
|||
import "github.com/elastic/apm-server/elasticsearch" |
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: unify imports for authorization means; in some files you import "github.com/elastic/apm-server/elasticsearch"
and in some "es github.com/elastic/apm-server/elasticsearch"
.
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 there any problem with that? when there is an exhaustive usage of the package, an alias can help, otherwise is just fine?
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.
There is no problem, purely consistency (it would just be super easy to unify, and makes it more readable). I marked the comment as nit
anyways.
elasticsearch/security_api.go
Outdated
|
||
// CreateAPIKey requires manage_security cluster privilege | ||
func CreateAPIKey(client Client, apikeyReq CreateApiKeyRequest) (CreateApiKeyResponse, error) { | ||
response := client.JSONRequest(http.MethodPut, "/_security/api_key", apikeyReq) |
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 go-elasticsearch client offers an API for all the security related requests. We should leverage that and use it rather than re-implementing it.
Also, imagine the ES endpoint changes with the next major version from /_security/api_key
to /_sec/api_key
(or whatever), then one would need to maintain different versions of the go-elasticsearch client AND check the endpoints for all supported ES versions again in our code.
This comment also concerns all the following methods.
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.
We should leverage that and use it rather than re-implementing it
The only thing we are missing from the client is crafting a request. It doesn't buy us that much, and it would mean that we'd have to duplicate every single function.
If there is a breaking change, we need to take care of it either way and in a similar manner, so can we tackle that problem when it comes?
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.
Why would we need to take care of a breaking change in the ES API if we use the provided go-elasticsearch client API? This API would abstract such ES API changes, right?
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.
Sure, I mean we would need 2 different implementations, one with each client.
That's the same with this approach, but at least like this we don't need 2 implementations for each function always, only when they diverge. Does that make sense?
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.
We should optimize for correctness. Bringing ES details into the APM Server code, rather than using the abstraction the go-elasticsearch client provides is error prone.
If doing so, you would need to add integration tests for every single function in the elasticsearch
package testing against v7 and v8 Elasticsearch instance to ensure breaking changes are recognized. That's duplicated effort, as already handled by the client.
The duplication of the clients API calls is ofc unfortunate, but they can be kept to a minimum, all the response decoding etc. you are doing doesn't need to be duplicated.
Maybe @karmi has some ideas on integrating with a v7
and v8
elasticsearch-go client at the same time, with minimum repetition while still leveraging the clients API.
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 thinking about something along the lines of https://github.com/jalvz/apm-server/compare/apikey-command...simitt:es-poc?expand=1
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 there's a compromise available. Since we're currently assuming the requests are compatible between v7 and v8 (the only difference in the code is which client is used for calling JSONRequest), then we could instead use the esapi request types from v7 and execute them with either client's transport. This keeps the ES API details inside the ES client.
https://gist.github.com/axw/8d9e3bd234faec19fa8ed265c460101d
We'll eventually need to have some code to deal with differences in the request and response bodies, and possibly request parameters. The ES low-level client doesn't abstract away the differences between versions. We could do that by doing something like below, however I think that's a much bigger issue that we should deal with later.
type clientV8 struct {
// embed clientV7 to get its methods
clientV7
}
// Foo in v8 is incompatible with Foo in v7, hence we override it.
func (c clientV8) Foo(req FooRequest) FooResponse {
reqV8 := fooV8Request{/*set fields from req*/}
var resp fooV8Response
...
return FooResponse{/*set fields from resp*/}
}
type clientV7 {
estransport.Transport // initialised with the transport from either v7 or v8
}
// Foo ...
func (c clientV7) Foo(req FooRequest) FooResponse {
reqV7 := fooV7Request{/*set fields from req*/}
var resp fooV7Response
...
return FooResponse{/*set fields from resp*/}
}
// Bar is compatible with v7 and v8, so we don't override it in clientV8.
func (c clientV7) Bar(req BarRequest) BarResponse {
reqV7 := barV7Request{/*set fields from req*/}
var resp barV7Response
...
return BarResponse{/*set fields from resp*/}
}
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.
Interesting idea @axw! As you pointed out, this only works if the actual requests for v7 and v8 can be assumed to be the same. Since you are using v7 and not v8 structs you ensure that the code runs fine with currently released ES versions.
+1 from me on moving forward with 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.
Ah, I see. Funny enough, I considered that but I thought it would be rejected because an implementation detail difference in one version could cause a bug when using the other version, and would be harder to detect than say a typo in a endpoint name.
I'm fine with that.
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.
Hello, I'm surprised that you went back to calling the ES endpoints manually, @jalvz — I remember we did find couple of ways of solving it when using the Go client, in our conversations.
I have particularly liked the "injection" of the client in one of the sketches you had, do you still have it handy?
I like the idea @axw is proposing.
I still do think that using the client for abstracting the communication with ES makes a lot of sense: manually crafting requests is something like an antipattern. I understand the challenges coming from the types being in different packages, but that's something we should be able to solve.
Codecov Report
@@ Coverage Diff @@
## master #3063 +/- ##
=========================================
Coverage ? 78.85%
=========================================
Files ? 106
Lines ? 5614
Branches ? 0
=========================================
Hits ? 4427
Misses ? 1187
Partials ? 0
|
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.
Some of these things can be deferred (e.g. renaming things), but please create TODOs and tracking issue(s). IMO the most important things to get fixed:
- revert
vendor/github.com/elastic/go-ucfg/error.go
- revert
beater/server_test.go
? Or maybe it's related, I don't know - update
cmd/apikey.go
to return errors to Cobra
|
||
deletion, err := es.DeletePrivileges(client, deletePrivilegesRequest) | ||
if err != nil { | ||
continue |
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.
Why is the error being silently ignored?
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.
IIRC, because it would fail to delete privileges if they don't exist. But I did this more than 1 month ago, my memory is fuzzy. Should check again
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.
Ah, because we're returning an error on 404. Probably shouldn't do that, but treat it as an expected status code. A problem for another day.
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.
isn't that what this it is doing? or you mean inside DeletePrivileges
impl? what difference would that make? 🤔
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 meant inside DeletePrivleges - we could also do it here, if we expose details of the error. This isn't checking for 404, it's ignoring all errors. The error could be because Elasticsearch is down, because the privilege doesn't exist, or because it's a Monday.
Co-Authored-By: Andrew Wilkins <[email protected]>
Co-Authored-By: Andrew Wilkins <[email protected]>
Co-Authored-By: Andrew Wilkins <[email protected]>
Co-Authored-By: Andrew Wilkins <[email protected]>
superseded by #3157 |
Still a couple tests failingUsage:
Closes #2987