Skip to content

Commit cbc9dc4

Browse files
authored
[Feature] JWT Rotation (#265)
1 parent bc1047a commit cbc9dc4

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

50 files changed

+2354
-370
lines changed

LICENSE.BOILERPLATE

Lines changed: 19 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,19 @@
1-
//
2-
// DISCLAIMER
3-
//
4-
// Copyright 2020 ArangoDB GmbH, Cologne, Germany
5-
//
6-
// Licensed under the Apache License, Version 2.0 (the "License");
7-
// you may not use this file except in compliance with the License.
8-
// You may obtain a copy of the License at
9-
//
10-
// http://www.apache.org/licenses/LICENSE-2.0
11-
//
12-
// Unless required by applicable law or agreed to in writing, software
13-
// distributed under the License is distributed on an "AS IS" BASIS,
14-
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15-
// See the License for the specific language governing permissions and
16-
// limitations under the License.
17-
//
18-
// Copyright holder is ArangoDB GmbH, Cologne, Germany
19-
//
1+
2+
DISCLAIMER
3+
4+
Copyright 2020 ArangoDB GmbH, Cologne, Germany
5+
6+
Licensed under the Apache License, Version 2.0 (the "License");
7+
you may not use this file except in compliance with the License.
8+
You may obtain a copy of the License at
9+
10+
http://www.apache.org/licenses/LICENSE-2.0
11+
12+
Unless required by applicable law or agreed to in writing, software
13+
distributed under the License is distributed on an "AS IS" BASIS,
14+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15+
See the License for the specific language governing permissions and
16+
limitations under the License.
17+
18+
Copyright holder is ArangoDB GmbH, Cologne, Germany
19+

Makefile

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -227,9 +227,15 @@ tools:
227227
@echo ">> Fetching github release"
228228
@go get -u github.com/aktau/github-release
229229

230+
231+
.PHONY: license
232+
license:
233+
@echo ">> Verify license of files"
234+
@go run github.com/google/addlicense -f "./LICENSE.BOILERPLATE" $(GO_SOURCES)
235+
230236
.PHONY: license-verify
231237
license-verify:
232-
@echo ">> Verify license of files"
238+
@echo ">> Ensuring license of files"
233239
@go run github.com/google/addlicense -f "./LICENSE.BOILERPLATE" -check $(GO_SOURCES)
234240

235241
.PHONY: fmt

admin.go

Lines changed: 200 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,200 @@
1+
//
2+
// DISCLAIMER
3+
//
4+
// Copyright 2018 ArangoDB GmbH, Cologne, Germany
5+
//
6+
// Licensed under the Apache License, Version 2.0 (the "License");
7+
// you may not use this file except in compliance with the License.
8+
// You may obtain a copy of the License at
9+
//
10+
// http://www.apache.org/licenses/LICENSE-2.0
11+
//
12+
// Unless required by applicable law or agreed to in writing, software
13+
// distributed under the License is distributed on an "AS IS" BASIS,
14+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15+
// See the License for the specific language governing permissions and
16+
// limitations under the License.
17+
//
18+
// Copyright holder is ArangoDB GmbH, Cologne, Germany
19+
//
20+
// Author Adam Janikowski
21+
//
22+
23+
package main
24+
25+
import (
26+
"context"
27+
"time"
28+
29+
"github.com/spf13/cobra"
30+
)
31+
32+
var (
33+
cmdAdmin = &cobra.Command{
34+
Use: "admin",
35+
Long: `ArangoDB Starter admin commands to manage and fetch current cluster state.`,
36+
}
37+
38+
cmdInventory = &cobra.Command{
39+
Use: "inventory",
40+
Long: `Fetch inventory details about members including versions, and if supported, JWT Checksums`,
41+
}
42+
43+
cmdJWT = &cobra.Command{
44+
Use: "jwt",
45+
Long: `Cluster JWT Management. Requires Enterprise 3.7.1+`,
46+
}
47+
48+
cmdJWTRefresh = &cobra.Command{
49+
Use: "refresh",
50+
Run: jwtRefresh,
51+
Long: `Reload local JWT Tokens from Starter JWT folder. Starter will remove obsolete keys if they are not used as Active on any member in inventory.`,
52+
}
53+
54+
cmdJWTActivate = &cobra.Command{
55+
Use: "activate",
56+
Run: jwtActivate,
57+
Long: `Activate JWT Token. Token needs to be installed on all instances, at least in passive mode.`,
58+
}
59+
60+
cmdInventoryLocal = &cobra.Command{
61+
Use: "local",
62+
Run: localInventory,
63+
Long: `Fetch inventory details about current starter instance members`,
64+
}
65+
66+
cmdInventoryCluster = &cobra.Command{
67+
Use: "cluster",
68+
Run: clusterInventory,
69+
Long: `Fetch inventory details about starter instances members from cluster`,
70+
}
71+
72+
adminOptions struct {
73+
starterEndpoint string
74+
}
75+
76+
jwtToken string
77+
)
78+
79+
func init() {
80+
f := cmdAdmin.PersistentFlags()
81+
f.StringVar(&adminOptions.starterEndpoint, "starter.endpoint", "http://localhost:8528", "The endpoint of the starter to connect to. E.g. http://localhost:8528")
82+
83+
cmdMain.AddCommand(cmdAdmin)
84+
85+
cmdAdmin.AddCommand(cmdInventory)
86+
87+
cmdAdmin.AddCommand(cmdJWT)
88+
89+
cmdJWTActivate.Flags().StringVar(&jwtToken, "token", "", "Token to be activated")
90+
91+
cmdJWT.AddCommand(cmdJWTActivate)
92+
93+
cmdJWT.AddCommand(cmdJWTRefresh)
94+
95+
cmdInventory.AddCommand(cmdInventoryLocal)
96+
97+
cmdInventory.AddCommand(cmdInventoryCluster)
98+
}
99+
100+
func localInventory(cmd *cobra.Command, args []string) {
101+
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
102+
defer cancel()
103+
c := mustCreateStarterClient(adminOptions.starterEndpoint)
104+
105+
i, err := c.Inventory(ctx)
106+
if err != nil {
107+
log.Fatal().Err(err).Msgf("Unable to load inventory")
108+
}
109+
110+
for n, m := range i.Members {
111+
if m.Error != nil {
112+
log.Error().Msgf("Member %s is in failed state: %s", n.String(), m.Error)
113+
}
114+
log.Info().Msgf("Member %s, Version %s, License: %s", n.String(), m.Version.Version, m.Version.License)
115+
116+
if m.Hashes != nil {
117+
log.Info().Msgf("Hashes:")
118+
log.Info().Msgf("\tJWT:")
119+
if a := m.Hashes.JWT.Active; a != nil {
120+
log.Info().Msgf("\t\tActive: %s", a.GetSHA())
121+
}
122+
if len(m.Hashes.JWT.Passive) != 0 {
123+
log.Info().Msgf("\t\tPassive:")
124+
for _, j := range m.Hashes.JWT.Passive {
125+
log.Info().Msgf("\t\t- %s", j.GetSHA())
126+
}
127+
}
128+
}
129+
}
130+
}
131+
132+
func clusterInventory(cmd *cobra.Command, args []string) {
133+
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
134+
defer cancel()
135+
c := mustCreateStarterClient(adminOptions.starterEndpoint)
136+
137+
i, err := c.ClusterInventory(ctx)
138+
if err != nil {
139+
log.Fatal().Err(err).Msgf("Unable to load inventory")
140+
}
141+
142+
for pn, p := range i.Peers {
143+
log.Info().Msgf("Peer %s, Members %d", pn, len(p.Members))
144+
for n, m := range p.Members {
145+
if m.Error != nil {
146+
log.Error().Msgf("\tMember %s is in failed state: %s", n.String(), m.Error)
147+
continue
148+
}
149+
log.Info().Msgf("\tMember %s, Version %s, License: %s", n.String(), m.Version.Version, m.Version.License)
150+
151+
if m.Hashes != nil {
152+
log.Info().Msgf("\tHashes:")
153+
log.Info().Msgf("\t\tJWT:")
154+
if a := m.Hashes.JWT.Active; a != nil {
155+
log.Info().Msgf("\t\t\tActive: %s", a.GetSHA())
156+
}
157+
if len(m.Hashes.JWT.Passive) != 0 {
158+
log.Info().Msgf("\t\t\tPassive:")
159+
for _, j := range m.Hashes.JWT.Passive {
160+
log.Info().Msgf("\t\t\t- %s", j.GetSHA())
161+
}
162+
}
163+
}
164+
}
165+
}
166+
}
167+
168+
func jwtRefresh(cmd *cobra.Command, args []string) {
169+
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
170+
defer cancel()
171+
c := mustCreateStarterClient(adminOptions.starterEndpoint)
172+
173+
log.Info().Msgf("Refreshing JWT Tokens")
174+
175+
_, err := c.AdminJWTRefresh(ctx)
176+
if err != nil {
177+
log.Fatal().Msgf("Error while refreshing JWT tokens: %s", err.Error())
178+
}
179+
180+
log.Info().Msgf("JWT Tokens refreshed")
181+
}
182+
183+
func jwtActivate(cmd *cobra.Command, args []string) {
184+
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
185+
defer cancel()
186+
c := mustCreateStarterClient(adminOptions.starterEndpoint)
187+
188+
log.Info().Msgf("Activating JWT Token")
189+
190+
if jwtToken == "" {
191+
log.Fatal().Msgf("JWT Token not provided")
192+
}
193+
194+
_, err := c.AdminJWTActivate(ctx, jwtToken)
195+
if err != nil {
196+
log.Fatal().Msgf("Error while activating JWT tokens: %s", err.Error())
197+
}
198+
199+
log.Info().Msgf("JWT Token %s activated", jwtToken)
200+
}

client/api.go

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,9 +25,13 @@ package client
2525
import (
2626
"context"
2727

28+
"github.com/arangodb-helper/arangodb/pkg/api"
29+
2830
driver "github.com/arangodb/go-driver"
2931
)
3032

33+
var _ API = &client{}
34+
3135
// API is the interface implemented by the starter's HTTP API's.
3236
type API interface {
3337
// ID requests the starters ID.
@@ -71,6 +75,14 @@ type API interface {
7175

7276
// Status returns the status of any upgrade plan
7377
UpgradeStatus(context.Context) (UpgradeStatus, error)
78+
79+
Inventory(ctx context.Context) (api.Inventory, error)
80+
81+
ClusterInventory(ctx context.Context) (api.ClusterInventory, error)
82+
83+
AdminJWTRefresh(ctx context.Context) (api.Empty, error)
84+
85+
AdminJWTActivate(ctx context.Context, token string) (api.Empty, error)
7486
}
7587

7688
// IDInfo contains the ID of the starter

client/client.go

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,8 @@ import (
3131
"net/url"
3232
"time"
3333

34+
"github.com/arangodb-helper/arangodb/pkg/api"
35+
3436
driver "github.com/arangodb/go-driver"
3537
"github.com/pkg/errors"
3638
)
@@ -53,6 +55,98 @@ type client struct {
5355
client *http.Client
5456
}
5557

58+
func (c *client) AdminJWTActivate(ctx context.Context, token string) (api.Empty, error) {
59+
var q = url.Values{}
60+
61+
q.Add("token", token)
62+
63+
url := c.createURL("/admin/jwt/activate", q)
64+
65+
var result api.Empty
66+
req, err := http.NewRequest("POST", url, nil)
67+
if err != nil {
68+
return api.Empty{}, maskAny(err)
69+
}
70+
if ctx != nil {
71+
req = req.WithContext(ctx)
72+
}
73+
resp, err := c.client.Do(req)
74+
if err != nil {
75+
return api.Empty{}, maskAny(err)
76+
}
77+
if err := c.handleResponse(resp, "POST", url, &result); err != nil {
78+
return api.Empty{}, maskAny(err)
79+
}
80+
81+
return result, nil
82+
}
83+
84+
func (c *client) AdminJWTRefresh(ctx context.Context) (api.Empty, error) {
85+
url := c.createURL("/admin/jwt/refresh", nil)
86+
87+
var result api.Empty
88+
req, err := http.NewRequest("POST", url, nil)
89+
if err != nil {
90+
return api.Empty{}, maskAny(err)
91+
}
92+
if ctx != nil {
93+
req = req.WithContext(ctx)
94+
}
95+
resp, err := c.client.Do(req)
96+
if err != nil {
97+
return api.Empty{}, maskAny(err)
98+
}
99+
if err := c.handleResponse(resp, "POST", url, &result); err != nil {
100+
return api.Empty{}, maskAny(err)
101+
}
102+
103+
return result, nil
104+
}
105+
106+
func (c *client) ClusterInventory(ctx context.Context) (api.ClusterInventory, error) {
107+
url := c.createURL("/cluster/inventory", nil)
108+
109+
var result api.ClusterInventory
110+
req, err := http.NewRequest("GET", url, nil)
111+
if err != nil {
112+
return api.ClusterInventory{}, maskAny(err)
113+
}
114+
if ctx != nil {
115+
req = req.WithContext(ctx)
116+
}
117+
resp, err := c.client.Do(req)
118+
if err != nil {
119+
return api.ClusterInventory{}, maskAny(err)
120+
}
121+
if err := c.handleResponse(resp, "GET", url, &result); err != nil {
122+
return api.ClusterInventory{}, maskAny(err)
123+
}
124+
125+
return result, nil
126+
}
127+
128+
func (c *client) Inventory(ctx context.Context) (api.Inventory, error) {
129+
url := c.createURL("/local/inventory", nil)
130+
131+
var result api.Inventory
132+
req, err := http.NewRequest("GET", url, nil)
133+
if err != nil {
134+
return api.Inventory{}, maskAny(err)
135+
}
136+
if ctx != nil {
137+
req = req.WithContext(ctx)
138+
}
139+
resp, err := c.client.Do(req)
140+
if err != nil {
141+
return api.Inventory{}, maskAny(err)
142+
}
143+
if err := c.handleResponse(resp, "GET", url, &result); err != nil {
144+
return api.Inventory{}, maskAny(err)
145+
}
146+
147+
return result, nil
148+
}
149+
56150
const (
57151
contentTypeJSON = "application/json"
58152
)

0 commit comments

Comments
 (0)