Skip to content

Commit

Permalink
Add rack access mechanism
Browse files Browse the repository at this point in the history
  • Loading branch information
nightfury1204 authored and MD NURE ALAM Nahid committed Jun 27, 2023
1 parent fad1d26 commit dcef21c
Show file tree
Hide file tree
Showing 57 changed files with 3,616 additions and 14 deletions.
2 changes: 2 additions & 0 deletions go.mod
Expand Up @@ -46,6 +46,8 @@ require (

require (
github.com/adrg/xdg v0.2.1 // indirect
github.com/golang-jwt/jwt/v4 v4.5.0 // indirect
github.com/satori/go.uuid v1.2.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)

Expand Down
3 changes: 3 additions & 0 deletions go.sum
Expand Up @@ -272,6 +272,8 @@ github.com/gofrs/uuid v3.2.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRx
github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
github.com/gogo/protobuf v1.2.1 h1:/s5zKNz0uPFCZ5hddgPdo2TK2TVrUNMn0OOX8/aZMTE=
github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4=
github.com/golang-jwt/jwt/v4 v4.5.0 h1:7cYmW1XlMY7h7ii7UhUyChSgS5wUJEnm9uZVTGqOWzg=
github.com/golang-jwt/jwt/v4 v4.5.0/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0=
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b h1:VKtxabqXZkF25pY9ekfRL6a582T4P37/31XEstQ5p58=
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
github.com/golang/groupcache v0.0.0-20190129154638-5b532d6fd5ef h1:veQD95Isof8w9/WXiA+pa3tz3fJXkt5B7QaRBrM62gk=
Expand Down Expand Up @@ -438,6 +440,7 @@ github.com/rogpeppe/go-internal v1.0.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFR
github.com/rogpeppe/go-internal v1.1.0 h1:g0fH8RicVgNl+zVZDCDfbdWxAWoAEJyI7I3TZYXFiig=
github.com/rogpeppe/go-internal v1.1.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/satori/go.uuid v1.2.0 h1:0uYX9dsZ2yD7q2RtLRtPSdGDWzjeM3TbMJP9utgA0ww=
github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0=
github.com/sebest/xff v0.0.0-20160910043805-6c115e0ffa35 h1:eajwn6K3weW5cd1ZXLu2sJ4pvwlBiCWY4uDejOr73gM=
github.com/sebest/xff v0.0.0-20160910043805-6c115e0ffa35/go.mod h1:wozgYq9WEBQBaIJe4YZ0qTSFAMxmcwBhQH0fO0R34Z0=
Expand Down
25 changes: 21 additions & 4 deletions pkg/api/api.go
@@ -1,8 +1,10 @@
package api

import (
"net/http"
"reflect"

"github.com/convox/rack/pkg/jwt"
"github.com/convox/rack/pkg/structs"
"github.com/convox/rack/provider"
"github.com/convox/stdapi"
Expand All @@ -12,6 +14,7 @@ type Server struct {
*stdapi.Server
Password string
Provider structs.Provider
JwtMngr *jwt.JwtManager
}

func New() (*Server, error) {
Expand All @@ -28,9 +31,15 @@ func NewWithProvider(p structs.Provider) *Server {
panic(err)
}

key, err := p.SystemJwtSignKey()
if err != nil {
panic(err)

Check warning on line 36 in pkg/api/api.go

View check run for this annotation

Codecov / codecov/patch

pkg/api/api.go#L36

Added line #L36 was not covered by tests
}

s := &Server{
Provider: p,
Server: stdapi.New("api", "api"),
JwtMngr: jwt.NewJwtManager(key),
}

s.Server.Router.Router = s.Server.Router.Router.SkipClean(true)
Expand All @@ -54,12 +63,20 @@ func NewWithProvider(p structs.Provider) *Server {

func (s *Server) authenticate(next stdapi.HandlerFunc) stdapi.HandlerFunc {
return func(c *stdapi.Context) error {
if _, pass, _ := c.Request().BasicAuth(); s.Password != "" && s.Password != pass {
return stdapi.Errorf(401, "invalid authentication")
username, pass, _ := c.Request().BasicAuth()
if username == "jwt" && s.JwtMngr != nil {
data, err := s.JwtMngr.Verify(pass)
if err != nil {
return stdapi.Errorf(http.StatusUnauthorized, "invalid authentication: %s", err)

Check warning on line 70 in pkg/api/api.go

View check run for this annotation

Codecov / codecov/patch

pkg/api/api.go#L68-L70

Added lines #L68 - L70 were not covered by tests
}
c.Set(structs.ConvoxRoleParam, data.Role)

Check warning on line 72 in pkg/api/api.go

View check run for this annotation

Codecov / codecov/patch

pkg/api/api.go#L72

Added line #L72 was not covered by tests
} else {
if s.Password != "" && s.Password != pass {
return stdapi.Errorf(http.StatusUnauthorized, "invalid authentication")
}
SetReadWriteRole(c)
}

SetReadWriteRole(c)

return next(c)
}
}
Expand Down
1 change: 1 addition & 0 deletions pkg/api/api_test.go
Expand Up @@ -19,6 +19,7 @@ func testServer(t *testing.T, fn func(*stdsdk.Client, *structs.MockProvider)) {
p := &structs.MockProvider{}
p.On("Initialize", mock.Anything).Return(nil)
p.On("WithContext", mock.Anything).Return(p).Maybe()
p.On("SystemJwtSignKey").Return("", nil)

s := api.NewWithProvider(p)
s.Logger = logger.Discard
Expand Down
1 change: 1 addition & 0 deletions pkg/api/auth_test.go
Expand Up @@ -18,6 +18,7 @@ import (
func TestAuthentication(t *testing.T) {
p := &structs.MockProvider{}
p.On("Initialize", mock.Anything).Return(nil)
p.On("SystemJwtSignKey").Return("", nil)

s := api.NewWithProvider(p)
s.Logger = logger.Discard
Expand Down
13 changes: 5 additions & 8 deletions pkg/api/authorization.go
Expand Up @@ -4,13 +4,10 @@ import (
"net/http"
"strings"

"github.com/convox/rack/pkg/structs"
"github.com/convox/stdapi"
)

const ConvoxRoleParam = "CONVOX_ROLE"
const ConvoxRoleRead = "r"
const ConvoxRoleReadWrite = "rw"

func (s *Server) Authorize(next stdapi.HandlerFunc) stdapi.HandlerFunc {
return func(c *stdapi.Context) error {
switch c.Request().Method {
Expand All @@ -28,25 +25,25 @@ func (s *Server) Authorize(next stdapi.HandlerFunc) stdapi.HandlerFunc {
}

func CanRead(c *stdapi.Context) bool {
if d := c.Get(ConvoxRoleParam); d != nil {
if d := c.Get(structs.ConvoxRoleParam); d != nil {
v, _ := d.(string)
return strings.Contains(v, "r")
}
return false
}

func CanWrite(c *stdapi.Context) bool {
if d := c.Get(ConvoxRoleParam); d != nil {
if d := c.Get(structs.ConvoxRoleParam); d != nil {
v, _ := d.(string)
return strings.Contains(v, "w")
}
return false

Check warning on line 40 in pkg/api/authorization.go

View check run for this annotation

Codecov / codecov/patch

pkg/api/authorization.go#L40

Added line #L40 was not covered by tests
}

func SetReadRole(c *stdapi.Context) {
c.Set(ConvoxRoleParam, ConvoxRoleRead)
c.Set(structs.ConvoxRoleParam, structs.ConvoxRoleRead)
}

func SetReadWriteRole(c *stdapi.Context) {
c.Set(ConvoxRoleParam, ConvoxRoleReadWrite)
c.Set(structs.ConvoxRoleParam, structs.ConvoxRoleReadWrite)
}
36 changes: 36 additions & 0 deletions pkg/api/controllers.go
Expand Up @@ -5,6 +5,7 @@ import (
"sort"
"strconv"
"strings"
"time"

"github.com/convox/rack/pkg/structs"
"github.com/convox/stdapi"
Expand Down Expand Up @@ -1176,6 +1177,41 @@ func (s *Server) SystemInstall(c *stdapi.Context) error {
return stdapi.Errorf(404, "not available via api")
}

func (s *Server) SystemJwtSignKeyRotate(c *stdapi.Context) error {
_, err := s.provider(c).WithContext(c.Context()).SystemJwtSignKeyRotate()
if err != nil {
return err

Check warning on line 1183 in pkg/api/controllers.go

View check run for this annotation

Codecov / codecov/patch

pkg/api/controllers.go#L1180-L1183

Added lines #L1180 - L1183 were not covered by tests
}
return c.RenderOK()

Check warning on line 1185 in pkg/api/controllers.go

View check run for this annotation

Codecov / codecov/patch

pkg/api/controllers.go#L1185

Added line #L1185 was not covered by tests
}

func (s *Server) SystemJwtToken(c *stdapi.Context) error {
role := c.Value("role")
durationInHour, err := strconv.Atoi(c.Value("durationInHour"))
if err != nil {
return stdapi.Errorf(404, "invalid duration")

Check warning on line 1192 in pkg/api/controllers.go

View check run for this annotation

Codecov / codecov/patch

pkg/api/controllers.go#L1188-L1192

Added lines #L1188 - L1192 were not covered by tests
}

var tk string

Check warning on line 1195 in pkg/api/controllers.go

View check run for this annotation

Codecov / codecov/patch

pkg/api/controllers.go#L1195

Added line #L1195 was not covered by tests

switch role {
case "read":
tk, err = s.JwtMngr.ReadToken(time.Hour * time.Duration(durationInHour))
if err != nil {
return err

Check warning on line 1201 in pkg/api/controllers.go

View check run for this annotation

Codecov / codecov/patch

pkg/api/controllers.go#L1197-L1201

Added lines #L1197 - L1201 were not covered by tests
}
case "write":
tk, err = s.JwtMngr.WriteToken(time.Hour * time.Duration(durationInHour))
if err != nil {
return err

Check warning on line 1206 in pkg/api/controllers.go

View check run for this annotation

Codecov / codecov/patch

pkg/api/controllers.go#L1203-L1206

Added lines #L1203 - L1206 were not covered by tests
}
}

return c.RenderJSON(map[string]string{
"token": tk,
})

Check warning on line 1212 in pkg/api/controllers.go

View check run for this annotation

Codecov / codecov/patch

pkg/api/controllers.go#L1210-L1212

Added lines #L1210 - L1212 were not covered by tests
}

func (s *Server) SystemLogs(c *stdapi.Context) error {
if err := s.hook("SystemLogsValidate", c); err != nil {
return err
Expand Down
2 changes: 2 additions & 0 deletions pkg/api/routes.go
Expand Up @@ -62,6 +62,8 @@ func (s *Server) setupRoutes(r stdapi.Router) {
r.Route("PUT", "/apps/{app}/services/{name}", s.ServiceUpdate)
r.Route("GET", "/system", s.SystemGet)
r.Route("", "", s.SystemInstall)
r.Route("PUT", "/system/jwt/rotate", s.SystemJwtSignKeyRotate)
r.Route("POST", "/system/jwt/token", s.SystemJwtToken)
r.Route("SOCKET", "/system/logs", s.SystemLogs)
r.Route("GET", "/system/metrics", s.SystemMetrics)
r.Route("GET", "/system/processes", s.SystemProcesses)
Expand Down
52 changes: 52 additions & 0 deletions pkg/cli/rack.go
Expand Up @@ -7,6 +7,7 @@ import (
"net/url"
"os"
"sort"
"strconv"
"strings"

"github.com/aws/aws-sdk-go/aws"
Expand All @@ -27,6 +28,20 @@ func init() {
Validate: stdcli.Args(0),
})

register("rack access", "get rack access creds", RackAccess, stdcli.CommandOptions{
Flags: []stdcli.Flag{
flagRack,
stdcli.StringFlag("role", "", "access role: read or write"),
stdcli.IntFlag("duration", "", "duration in hours"),
},
Validate: stdcli.Args(0),
})

register("rack access key rotate", "rotate access key", RackAccessKeyRotate, stdcli.CommandOptions{
Flags: []stdcli.Flag{flagRack},
Validate: stdcli.Args(0),
})

registerWithoutProvider("rack install", "install a rack", RackInstall, stdcli.CommandOptions{
Flags: append(stdcli.OptionFlags(structs.SystemInstallOptions{})),
Usage: "<type> [Parameter=Value]...",
Expand Down Expand Up @@ -128,6 +143,43 @@ func Rack(rack sdk.Interface, c *stdcli.Context) error {
return i.Print()
}

func RackAccess(rack sdk.Interface, c *stdcli.Context) error {
rData, err := rack.SystemGet()
if err != nil {
return err

Check warning on line 149 in pkg/cli/rack.go

View check run for this annotation

Codecov / codecov/patch

pkg/cli/rack.go#L146-L149

Added lines #L146 - L149 were not covered by tests
}

role, ok := c.Value("role").(string)
if !ok {
return fmt.Errorf("role is required")

Check warning on line 154 in pkg/cli/rack.go

View check run for this annotation

Codecov / codecov/patch

pkg/cli/rack.go#L152-L154

Added lines #L152 - L154 were not covered by tests
}

duration, ok := c.Value("duration").(int)
if !ok {
return fmt.Errorf("duration is required")

Check warning on line 159 in pkg/cli/rack.go

View check run for this annotation

Codecov / codecov/patch

pkg/cli/rack.go#L157-L159

Added lines #L157 - L159 were not covered by tests
}

jwtTk, err := rack.SystemJwtToken(structs.SystemJwtOptions{
Role: options.String(role),
DurationInHour: options.String(strconv.Itoa(duration)),
})
if err != nil {
fmt.Println(err)
return err

Check warning on line 168 in pkg/cli/rack.go

View check run for this annotation

Codecov / codecov/patch

pkg/cli/rack.go#L162-L168

Added lines #L162 - L168 were not covered by tests
}

return c.Writef("RACK_URL=https://jwt:%s@%s\n", jwtTk.Token, rData.RackDomain)

Check warning on line 171 in pkg/cli/rack.go

View check run for this annotation

Codecov / codecov/patch

pkg/cli/rack.go#L171

Added line #L171 was not covered by tests
}

func RackAccessKeyRotate(rack sdk.Interface, c *stdcli.Context) error {
_, err := rack.SystemJwtSignKeyRotate()
if err != nil {
return err

Check warning on line 177 in pkg/cli/rack.go

View check run for this annotation

Codecov / codecov/patch

pkg/cli/rack.go#L174-L177

Added lines #L174 - L177 were not covered by tests
}

return c.OK()

Check warning on line 180 in pkg/cli/rack.go

View check run for this annotation

Codecov / codecov/patch

pkg/cli/rack.go#L180

Added line #L180 was not covered by tests
}

func RackInstall(rack sdk.Interface, c *stdcli.Context) error {
var opts structs.SystemInstallOptions

Expand Down
85 changes: 85 additions & 0 deletions pkg/jwt/jwt.go
@@ -0,0 +1,85 @@
package jwt

import (
"fmt"
"time"

"github.com/convox/rack/pkg/structs"
"github.com/golang-jwt/jwt/v4"
)

type TokenData struct {
User string
Role string
ExpiresAt time.Time
}

type JwtManager struct {
signKey []byte
}

func NewJwtManager(signKey string) *JwtManager {
return &JwtManager{
signKey: []byte(signKey),
}
}

func (j *JwtManager) ReadToken(duration time.Duration) (string, error) {
token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{
"user": "system-read",
"role": structs.ConvoxRoleRead,
"expiresAt": time.Now().UTC().Add(duration).Unix(),
})

// Sign and get the complete encoded token as a string using the secret
tokenString, err := token.SignedString(j.signKey)
if err != nil {
return "", err

Check warning on line 37 in pkg/jwt/jwt.go

View check run for this annotation

Codecov / codecov/patch

pkg/jwt/jwt.go#L37

Added line #L37 was not covered by tests
}
return tokenString, nil
}

func (j *JwtManager) WriteToken(duration time.Duration) (string, error) {
token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{
"user": "system-write",
"role": structs.ConvoxRoleReadWrite,
"expiresAt": time.Now().UTC().Add(duration).Unix(),
})

// Sign and get the complete encoded token as a string using the secret
tokenString, err := token.SignedString(j.signKey)
if err != nil {
return "", err

Check warning on line 52 in pkg/jwt/jwt.go

View check run for this annotation

Codecov / codecov/patch

pkg/jwt/jwt.go#L52

Added line #L52 was not covered by tests
}
return tokenString, nil
}

func (j *JwtManager) Verify(token string) (*TokenData, error) {
d := &TokenData{}
tk, err := jwt.Parse(token, func(t *jwt.Token) (interface{}, error) {
if _, ok := t.Method.(*jwt.SigningMethodHMAC); !ok {
return nil, fmt.Errorf("unexpected signing method: %v", t.Header["alg"])

Check warning on line 61 in pkg/jwt/jwt.go

View check run for this annotation

Codecov / codecov/patch

pkg/jwt/jwt.go#L61

Added line #L61 was not covered by tests
}

return j.signKey, nil
})
if err != nil {
return nil, err
}

if !tk.Valid {
return nil, fmt.Errorf("invalid token")

Check warning on line 71 in pkg/jwt/jwt.go

View check run for this annotation

Codecov / codecov/patch

pkg/jwt/jwt.go#L71

Added line #L71 was not covered by tests
}
if claims, ok := tk.Claims.(jwt.MapClaims); ok {
d.User = claims["user"].(string)
d.Role = claims["role"].(string)
expiresAt := (int64)(claims["expiresAt"].(float64))
d.ExpiresAt = time.Unix(expiresAt, 0)
if d.ExpiresAt.UTC().Before(time.Now().UTC()) {
return nil, fmt.Errorf("token is expired")
}
} else {
return nil, fmt.Errorf("invalid token")

Check warning on line 82 in pkg/jwt/jwt.go

View check run for this annotation

Codecov / codecov/patch

pkg/jwt/jwt.go#L81-L82

Added lines #L81 - L82 were not covered by tests
}
return d, nil
}

0 comments on commit dcef21c

Please sign in to comment.