Skip to content

Commit fbd5e7a

Browse files
authored
Merge pull request #18 from arangodb-helper/jwt-authentication
Authentication
2 parents 84ffd2e + dce477f commit fbd5e7a

Some content is hidden

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

53 files changed

+3684
-31
lines changed

main.go

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package main
33
import (
44
"context"
55
"fmt"
6+
"io/ioutil"
67
"os"
78
"os/signal"
89
"path/filepath"
@@ -47,6 +48,7 @@ var (
4748
verbose bool
4849
serverThreads int
4950
allPortOffsetsUnique bool
51+
jwtSecretFile string
5052
dockerEndpoint string
5153
dockerImage string
5254
dockerUser string
@@ -79,6 +81,7 @@ func init() {
7981
f.BoolVar(&dockerNetHost, "dockerNetHost", false, "Run containers with --net=host")
8082
f.BoolVar(&dockerPrivileged, "dockerPrivileged", false, "Run containers with --privileged")
8183
f.BoolVar(&allPortOffsetsUnique, "uniquePortOffsets", false, "If set, all peers will get a unique port offset. If false (default) only portOffset+peerAddress pairs will be unique.")
84+
f.StringVar(&jwtSecretFile, "jwtSecretFile", "", "name of a plain text file containing a JWT secret used for server authentication")
8285
}
8386

8487
// handleSignal listens for termination signals and stops this process onup termination.
@@ -191,6 +194,16 @@ func cmdMainRun(cmd *cobra.Command, args []string) {
191194
log.Fatalf("Cannot create data directory %s because %v, giving up.", dataDir, err)
192195
}
193196

197+
// Read jwtSecret (if any)
198+
var jwtSecret string
199+
if jwtSecretFile != "" {
200+
content, err := ioutil.ReadFile(jwtSecretFile)
201+
if err != nil {
202+
log.Fatalf("Failed to read JWT secret file '%s': %v", jwtSecretFile, err)
203+
}
204+
jwtSecret = strings.TrimSpace(string(content))
205+
}
206+
194207
// Interrupt signal:
195208
sigChannel := make(chan os.Signal)
196209
rootCtx, cancel := context.WithCancel(context.Background())
@@ -213,6 +226,7 @@ func cmdMainRun(cmd *cobra.Command, args []string) {
213226
Verbose: verbose,
214227
ServerThreads: serverThreads,
215228
AllPortOffsetsUnique: allPortOffsetsUnique,
229+
JwtSecret: jwtSecret,
216230
RunningInDocker: os.Getenv("RUNNING_IN_DOCKER") == "true",
217231
DockerContainer: dockerContainer,
218232
DockerEndpoint: dockerEndpoint,

service/arangodb.go

Lines changed: 68 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ type ServiceConfig struct {
3434
Verbose bool
3535
ServerThreads int // If set to something other than 0, this will be added to the commandline of each server with `--server.threads`...
3636
AllPortOffsetsUnique bool // If set, all peers will get a unique port offset. If false (default) only portOffset+peerAddress pairs will be unique.
37+
JwtSecret string
3738

3839
DockerContainer string // Name of the container running this process
3940
DockerEndpoint string // Where to reach the docker daemon
@@ -130,15 +131,32 @@ func slasher(s string) string {
130131
return strings.Replace(s, "\\", "/", -1)
131132
}
132133

133-
func testInstance(ctx context.Context, address string, port int) (up, cancelled bool) {
134+
func (s *Service) testInstance(ctx context.Context, address string, port int) (up, cancelled bool) {
134135
instanceUp := make(chan bool)
135136
go func() {
136137
client := &http.Client{Timeout: time.Second * 10}
137-
for i := 0; i < 300; i++ {
138+
makeRequest := func() error {
138139
addr := net.JoinHostPort(address, strconv.Itoa(port))
139140
url := fmt.Sprintf("http://%s/_api/version", addr)
140-
r, e := client.Get(url)
141-
if e == nil && r != nil && r.StatusCode == 200 {
141+
req, err := http.NewRequest("GET", url, nil)
142+
if err != nil {
143+
return maskAny(err)
144+
}
145+
if err := addJwtHeader(req, s.JwtSecret); err != nil {
146+
return maskAny(err)
147+
}
148+
resp, err := client.Do(req)
149+
if err != nil {
150+
return maskAny(err)
151+
}
152+
if resp.StatusCode != 200 {
153+
return maskAny(fmt.Errorf("Invalid status %d", resp.StatusCode))
154+
}
155+
return nil
156+
}
157+
158+
for i := 0; i < 300; i++ {
159+
if err := makeRequest(); err == nil {
142160
instanceUp <- true
143161
break
144162
}
@@ -154,23 +172,6 @@ func testInstance(ctx context.Context, address string, port int) (up, cancelled
154172
}
155173
}
156174

157-
var confFileTemplate = `# ArangoDB configuration file
158-
#
159-
# Documentation:
160-
# https://docs.arangodb.com/Manual/Administration/Configuration/
161-
#
162-
163-
[server]
164-
endpoint = tcp://[::]:%s
165-
threads = %d
166-
167-
[log]
168-
level = %s
169-
170-
[javascript]
171-
v8-contexts = %d
172-
`
173-
174175
func (s *Service) makeBaseArgs(myHostDir, myContainerDir string, myAddress string, myPort string, mode string) (args []string, configVolumes []Volume) {
175176
hostConfFileName := filepath.Join(myHostDir, "arangod.conf")
176177
containerConfFileName := filepath.Join(myContainerDir, "arangod.conf")
@@ -184,20 +185,57 @@ func (s *Service) makeBaseArgs(myHostDir, myContainerDir string, myAddress strin
184185
}
185186

186187
if _, err := os.Stat(hostConfFileName); os.IsNotExist(err) {
187-
out, e := os.Create(hostConfFileName)
188-
if e != nil {
189-
s.log.Fatalf("Could not create configuration file %s, error: %#v", hostConfFileName, e)
190-
}
188+
var threads, v8Contexts string
189+
logLevel := "INFO"
191190
switch mode {
192191
// Parameters are: port, server threads, log level, v8-contexts
193192
case "agent":
194-
fmt.Fprintf(out, confFileTemplate, myPort, 8, "INFO", 1)
193+
threads = "8"
194+
v8Contexts = "1"
195195
case "dbserver":
196-
fmt.Fprintf(out, confFileTemplate, myPort, 4, "INFO", 4)
196+
threads = "4"
197+
v8Contexts = "4"
197198
case "coordinator":
198-
fmt.Fprintf(out, confFileTemplate, myPort, 16, "INFO", 4)
199+
threads = "16"
200+
v8Contexts = "4"
201+
}
202+
serverSection := &configSection{
203+
Name: "server",
204+
Settings: map[string]string{
205+
"endpoint": fmt.Sprintf("tcp://[::]:%s", myPort),
206+
"threads": threads,
207+
"authentication": "false",
208+
},
209+
}
210+
if s.JwtSecret != "" {
211+
serverSection.Settings["authentication"] = "true"
212+
serverSection.Settings["jwt-secret"] = s.JwtSecret
213+
}
214+
config := configFile{
215+
serverSection,
216+
&configSection{
217+
Name: "log",
218+
Settings: map[string]string{
219+
"level": logLevel,
220+
},
221+
},
222+
&configSection{
223+
Name: "javascript",
224+
Settings: map[string]string{
225+
"v8-contexts": v8Contexts,
226+
},
227+
},
199228
}
229+
230+
out, e := os.Create(hostConfFileName)
231+
if e != nil {
232+
s.log.Fatalf("Could not create configuration file %s, error: %#v", hostConfFileName, e)
233+
}
234+
_, err := config.WriteTo(out)
200235
out.Close()
236+
if err != nil {
237+
s.log.Fatalf("Cannot create config file: %v", err)
238+
}
201239
}
202240
args = make([]string, 0, 40)
203241
executable := s.ArangodExecutable
@@ -213,7 +251,6 @@ func (s *Service) makeBaseArgs(myHostDir, myContainerDir string, myAddress strin
213251
"--javascript.app-path", slasher(filepath.Join(myContainerDir, "apps")),
214252
"--log.file", slasher(filepath.Join(myContainerDir, "arangod.log")),
215253
"--log.force-direct", "false",
216-
"--server.authentication", "false",
217254
)
218255
if s.ServerThreads != 0 {
219256
args = append(args, "--server.threads", strconv.Itoa(s.ServerThreads))
@@ -319,7 +356,7 @@ func (s *Service) startRunning(runner Runner) {
319356
if p != nil {
320357
s.log.Infof("%s seems to be running already, checking port %d...", mode, myPort)
321358
ctx, cancel := context.WithTimeout(context.Background(), time.Second*10)
322-
up, _ := testInstance(ctx, myHost, myPort)
359+
up, _ := s.testInstance(ctx, myHost, myPort)
323360
cancel()
324361
if up {
325362
s.log.Infof("%s is already running on %d. No need to start anything.", mode, myPort)
@@ -360,7 +397,7 @@ func (s *Service) startRunning(runner Runner) {
360397
*processVar = p
361398
ctx, cancel := context.WithCancel(s.ctx)
362399
go func() {
363-
if up, cancelled := testInstance(ctx, myHost, s.MasterPort+portOffset+serverPortOffset); !cancelled {
400+
if up, cancelled := s.testInstance(ctx, myHost, s.MasterPort+portOffset+serverPortOffset); !cancelled {
364401
if up {
365402
s.log.Infof("%s up and running.", mode)
366403
} else {

service/config.go

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
package service
2+
3+
import (
4+
"fmt"
5+
"io"
6+
"strings"
7+
)
8+
9+
var confHeader = `# ArangoDB configuration file
10+
#
11+
# Documentation:
12+
# https://docs.arangodb.com/Manual/Administration/Configuration/
13+
#
14+
15+
`
16+
17+
type configFile []*configSection
18+
19+
// WriteTo writes the configuration sections to the given writer.
20+
func (cf configFile) WriteTo(w io.Writer) (int64, error) {
21+
x := int64(0)
22+
n, err := w.Write([]byte(confHeader))
23+
if err != nil {
24+
return x, maskAny(err)
25+
}
26+
x += int64(n)
27+
for _, section := range cf {
28+
n, err := section.WriteTo(w)
29+
if err != nil {
30+
return x, maskAny(err)
31+
}
32+
x += int64(n)
33+
}
34+
return x, nil
35+
}
36+
37+
type configSection struct {
38+
Name string
39+
Settings map[string]string
40+
}
41+
42+
// WriteTo writes the configuration section to the given writer.
43+
func (s *configSection) WriteTo(w io.Writer) (int64, error) {
44+
lines := []string{"[" + s.Name + "]"}
45+
for k, v := range s.Settings {
46+
lines = append(lines, fmt.Sprintf("%s = %s", k, v))
47+
}
48+
lines = append(lines, "")
49+
n, err := w.Write([]byte(strings.Join(lines, "\n")))
50+
return int64(n), maskAny(err)
51+
}

service/jwt.go

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
package service
2+
3+
import (
4+
"net/http"
5+
6+
jwt "github.com/dgrijalva/jwt-go"
7+
)
8+
9+
// addJwtHeader calculates a JWT authorization header based on the given secret
10+
// and adds it to the given request.
11+
// If the secret is empty, nothing is done.
12+
func addJwtHeader(req *http.Request, jwtSecret string) error {
13+
if jwtSecret == "" {
14+
return nil
15+
}
16+
// Create a new token object, specifying signing method and the claims
17+
// you would like it to contain.
18+
token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{
19+
"iss": "arangodb",
20+
"server_id": "foo",
21+
})
22+
23+
// Sign and get the complete encoded token as a string using the secret
24+
signedToken, err := token.SignedString([]byte(jwtSecret))
25+
if err != nil {
26+
return maskAny(err)
27+
}
28+
29+
req.Header.Set("Authorization", "bearer "+signedToken)
30+
return nil
31+
}

vendor/github.com/dgrijalva/jwt-go/.gitignore

Lines changed: 4 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

vendor/github.com/dgrijalva/jwt-go/.travis.yml

Lines changed: 13 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

vendor/github.com/dgrijalva/jwt-go/LICENSE

Lines changed: 8 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)