Skip to content

Commit f4692a9

Browse files
anatol-sialitskialansemenov
authored andcommitted
Support XP7.15 Service Accounts auth #580
1 parent e210d49 commit f4692a9

File tree

27 files changed

+137
-37
lines changed

27 files changed

+137
-37
lines changed

go.mod

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ require (
2828
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e // indirect
2929
github.com/cpuguy83/go-md2man/v2 v2.0.5 // indirect
3030
github.com/emirpasic/gods v1.12.0 // indirect
31+
github.com/golang-jwt/jwt/v5 v5.2.1 // indirect
3132
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect
3233
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 // indirect
3334
github.com/kevinburke/ssh_config v0.0.0-20190725054713-01f96b0aa0cd // indirect

go.sum

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,8 @@ github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/
3535
github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568/go.mod h1:xEzjJPgXI435gkrCt3MPfRiAkVrwSbHsst4LCFVfpJc=
3636
github.com/gliderlabs/ssh v0.2.2 h1:6zsha5zo/TWhRhwqCD3+EarCAgZ2yN28ipRnGPnwkI0=
3737
github.com/gliderlabs/ssh v0.2.2/go.mod h1:U7qILu1NlMHj9FlMhZLlkCdDnU1DBEAqr0aevW3Awn0=
38+
github.com/golang-jwt/jwt/v5 v5.2.1 h1:OuVbFODueb089Lh128TAcimifWaLhJwVflnrgM17wHk=
39+
github.com/golang-jwt/jwt/v5 v5.2.1/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
3840
github.com/google/go-cmp v0.3.0 h1:crn/baboCvb5fXaQ0IJ1SGTsTVrWpDsCWC8EGETZijY=
3941
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
4042
github.com/hinshun/vt10x v0.0.0-20180616224451-1954e6464174 h1:WlZsjVhE8Af9IcZDGgJGQpNflI3+MJSBhsgT5PCtzBQ=

internal/app/commands/app/install.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ var Install = cli.Command{
3131
Name: "file",
3232
Usage: "Application file",
3333
},
34-
}, common.AUTH_FLAG, common.FORCE_FLAG),
34+
}, common.AUTH_FLAG, common.CRED_FILE_FLAG, common.FORCE_FLAG),
3535
Action: func(c *cli.Context) error {
3636

3737
file, url := ensureURLOrFileFlag(c)

internal/app/commands/app/start.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ import (
1313
var Start = cli.Command{
1414
Name: "start",
1515
Usage: "Start an application",
16-
Flags: append([]cli.Flag{}, common.AUTH_FLAG, common.FORCE_FLAG),
16+
Flags: append([]cli.Flag{}, common.AUTH_FLAG, common.CRED_FILE_FLAG, common.FORCE_FLAG),
1717
ArgsUsage: "<app key>",
1818
Action: func(c *cli.Context) error {
1919

internal/app/commands/app/stop.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ var Stop = cli.Command{
1414
Name: "stop",
1515
Usage: "Stop an application",
1616
ArgsUsage: "<app key>",
17-
Flags: append([]cli.Flag{}, common.AUTH_FLAG, common.FORCE_FLAG),
17+
Flags: append([]cli.Flag{}, common.AUTH_FLAG, common.CRED_FILE_FLAG, common.FORCE_FLAG),
1818
Action: func(c *cli.Context) error {
1919

2020
key := ensureAppKeyArg(c)

internal/app/commands/auditlog/cleanup.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ var Cleanup = cli.Command{
2222
Name: "age",
2323
Usage: "Age of records to be removed. The format based on the ISO-8601 duration format PnDTnHnMn.nS with days considered to be exactly 24 hours.",
2424
},
25-
}, common.AUTH_FLAG, common.FORCE_FLAG),
25+
}, common.AUTH_FLAG, common.CRED_FILE_FLAG, common.FORCE_FLAG),
2626
Action: func(c *cli.Context) error {
2727

2828
age := ensureAgeParam(c)

internal/app/commands/cms/reprocess.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ var Reprocess = cli.Command{
2626
Name: "skip-children",
2727
Usage: "Flag to skip processing of content children.",
2828
},
29-
}, common.AUTH_FLAG, common.FORCE_FLAG),
29+
}, common.AUTH_FLAG, common.CRED_FILE_FLAG, common.FORCE_FLAG),
3030
Action: func(c *cli.Context) error {
3131

3232
var result ReprocessResponse

internal/app/commands/common/common.go

Lines changed: 47 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,11 @@ var AUTH_FLAG = cli.StringFlag{
5757
Usage: "Authentication token for basic authentication (user:password)",
5858
}
5959

60+
var CRED_FILE_FLAG = cli.StringFlag{
61+
Name: "cred-file",
62+
Usage: "The path to the service account key file (in JSON format). This is only available for XP version 7.15 and later. Key file can be generated by Users application for System ID Provider users (aka Service Accounts). If specified, the flag \"--auth\" or \"-a\" will be ignored",
63+
}
64+
6065
var FORCE_FLAG = cli.BoolFlag{
6166
Name: "force, f",
6267
Usage: "Accept default answers to all prompts and run non-interactively",
@@ -212,13 +217,29 @@ func EnsureAuth(authString string, force bool) (string, string) {
212217
return splitAuth[0], splitAuth[1]
213218
}
214219

220+
func resolveCredFilePath(path string) string {
221+
if path != "" {
222+
return path
223+
} else if pathFromEnv := os.Getenv("ENONIC_CLI_CRED_FILE"); pathFromEnv != "" {
224+
return pathFromEnv
225+
} else {
226+
return ""
227+
}
228+
}
229+
215230
func CreateRequest(c *cli.Context, method, url string, body io.Reader) *http.Request {
216-
var auth, user, pass string
231+
var auth, user, pass, credFilePath string
217232
if c != nil {
218233
auth = c.String("auth")
234+
credFilePath = resolveCredFilePath(c.String("cred-file"))
219235
}
220236

221-
if url != MARKET_URL && url != SCOOP_MANIFEST_URL && (ReadRuntimeData().SessionID == "" || auth != "") {
237+
if url != MARKET_URL && url != SCOOP_MANIFEST_URL && (ReadRuntimeData().SessionID == "" || auth != "" || credFilePath != "") {
238+
if credFilePath != "" {
239+
jwtToken := generateServiceAccountJwtToken(credFilePath)
240+
return doCreateRequestWithBearerToken(method, url, jwtToken, body)
241+
}
242+
222243
if auth == "" {
223244
activeRemote := remote.GetActiveRemote()
224245
if activeRemote.User != "" || activeRemote.Pass != "" {
@@ -231,19 +252,19 @@ func CreateRequest(c *cli.Context, method, url string, body io.Reader) *http.Req
231252
return doCreateRequest(method, url, user, pass, body, IsForceMode(c))
232253
}
233254

234-
func doCreateRequest(method, reqUrl, user, pass string, body io.Reader, force bool) *http.Request {
255+
func doCreateSimpleRequest(method, reqUrl string, body io.Reader) *http.Request {
235256
var (
236-
host, scheme, port, path string
257+
host, scheme, port, restPath string
237258
)
238259

239260
parsedUrl, err := url.Parse(reqUrl)
240-
util.Fatal(err, "Not a valid url: "+reqUrl)
261+
util.Fatal(err, fmt.Sprintf("Not a valid url: %s", reqUrl))
241262

242263
if parsedUrl.IsAbs() {
243264
host = parsedUrl.Hostname()
244265
port = parsedUrl.Port()
245266
scheme = parsedUrl.Scheme
246-
path = parsedUrl.Path
267+
restPath = parsedUrl.Path
247268
} else {
248269
activeRemote := remote.GetActiveRemote()
249270
host = activeRemote.Url.Hostname()
@@ -253,18 +274,31 @@ func doCreateRequest(method, reqUrl, user, pass string, body io.Reader, force bo
253274
runeUrl := []rune(reqUrl)
254275
if runeUrl[0] == '/' {
255276
// absolute path
256-
path = reqUrl
277+
restPath = reqUrl
257278
} else {
258279
// relative path
259-
path = activeRemote.Url.Path + "/" + reqUrl
280+
restPath = activeRemote.Url.Path + "/" + reqUrl
260281
}
261282
}
262283

263-
req, err := http.NewRequest(method, fmt.Sprintf("%s://%s:%s%s", scheme, host, port, path), body)
264-
if err != nil {
265-
fmt.Fprintln(os.Stderr, "Params error: ", err)
266-
os.Exit(1)
267-
}
284+
req, err := http.NewRequest(method, fmt.Sprintf("%s://%s:%s%s", scheme, host, port, restPath), body)
285+
util.Fatal(err, "Params error: ")
286+
287+
return req
288+
}
289+
290+
func doCreateRequestWithBearerToken(method, reqUrl, jwtToken string, body io.Reader) *http.Request {
291+
req := doCreateSimpleRequest(method, reqUrl, body)
292+
293+
req.Header.Set("Content-Type", "application/json")
294+
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", jwtToken))
295+
296+
return req
297+
}
298+
299+
func doCreateRequest(method, reqUrl, user, pass string, body io.Reader, force bool) *http.Request {
300+
req := doCreateSimpleRequest(method, reqUrl, body)
301+
268302
req.Header.Set("Content-Type", "application/json")
269303
req.AddCookie(&http.Cookie{Name: FORCE_COOKIE, Value: strconv.FormatBool(force)})
270304

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
package common
2+
3+
import (
4+
"cli-enonic/internal/app/util"
5+
"encoding/json"
6+
"fmt"
7+
"github.com/golang-jwt/jwt/v5"
8+
"github.com/pkg/errors"
9+
"os"
10+
"strings"
11+
"time"
12+
)
13+
14+
type ServiceAccountData struct {
15+
Algorithm string `json:"algorithm"`
16+
Kid string `json:"kid"`
17+
Label string `json:"label"`
18+
PrincipalKey string `json:"principalKey"`
19+
PrivateKey string `json:"privateKey"`
20+
}
21+
22+
func hasJsonExtension(filepath string) bool {
23+
return strings.ToLower(filepath[len(filepath)-5:]) == ".json"
24+
}
25+
26+
func parseServiceAccountData(credFilePath string) ServiceAccountData {
27+
if !hasJsonExtension(credFilePath) {
28+
util.Fatal(errors.New(fmt.Sprintf("Error: %s is not a JSON file", credFilePath)), "")
29+
}
30+
31+
fileData, err := os.ReadFile(credFilePath)
32+
util.Fatal(err, fmt.Sprintf("Error reading JSON file: %v", err))
33+
34+
var serviceAccountData ServiceAccountData
35+
if err := json.Unmarshal(fileData, &serviceAccountData); err != nil {
36+
util.Fatal(err, fmt.Sprintf("Error parsing JSON: %v", err))
37+
}
38+
return serviceAccountData
39+
}
40+
41+
func generateServiceAccountJwtToken(credFilePath string) string {
42+
serviceAccountData := parseServiceAccountData(credFilePath)
43+
44+
privateKey, err := jwt.ParseRSAPrivateKeyFromPEM([]byte(serviceAccountData.PrivateKey))
45+
util.Fatal(err, fmt.Sprintf("Error parsing private key: %v", err))
46+
47+
now := time.Now()
48+
49+
claims := jwt.MapClaims{
50+
"sub": serviceAccountData.PrincipalKey,
51+
"iat": now.Unix(),
52+
"exp": now.Add(30 * time.Second).Unix(),
53+
}
54+
55+
token := jwt.NewWithClaims(jwt.SigningMethodRS256, claims)
56+
57+
token.Header["kid"] = serviceAccountData.Kid
58+
59+
signedToken, err := token.SignedString(privateKey)
60+
util.Fatal(err, fmt.Sprintf("Error signing token: %v", err))
61+
62+
return signedToken
63+
}

internal/app/commands/dump/list.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ var List = cli.Command{
1414
Name: "list",
1515
Aliases: []string{"ls"},
1616
Usage: "List available dumps",
17-
Flags: append([]cli.Flag{}, common.AUTH_FLAG, common.FORCE_FLAG),
17+
Flags: append([]cli.Flag{}, common.AUTH_FLAG, common.CRED_FILE_FLAG, common.FORCE_FLAG),
1818
Action: func(c *cli.Context) error {
1919

2020
dumps := listExistingDumpNames()

0 commit comments

Comments
 (0)