Skip to content

Commit

Permalink
[#3664] Feat: Add support for rack access
Browse files Browse the repository at this point in the history
### What is the feature/fix?
https://app.asana.com/0/1203637156732418/1204402996040452/f

### Add screenshot or video (optional)

** Any screenshot or video capture using the feature **

### Does it has a breaking change?

** Describe the changes and if it has any breaking changes in any feature **

### How to use/test it?

** Describe how to test or use the feature **

### Checklist
- [ ] New coverage tests
- [ ] Unit tests passing
- [ ] E2E tests passing
- [ ] E2E downgrade/update test passing
- [ ] Documentation updated
- [ ] No warnings or errors on Deepsource/Codecov
  • Loading branch information
nightfury1204 committed Jul 4, 2023
1 parent c311060 commit 1361189
Show file tree
Hide file tree
Showing 58 changed files with 3,728 additions and 4 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
24 changes: 22 additions & 2 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)
}

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,9 +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)
}
c.Set(structs.ConvoxRoleParam, data.Role)
} else {
if s.Password != "" && s.Password != pass {
return stdapi.Errorf(http.StatusUnauthorized, "invalid authentication")
}
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
49 changes: 49 additions & 0 deletions pkg/api/authorization.go
@@ -0,0 +1,49 @@
package api

import (
"net/http"
"strings"

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

func (s *Server) Authorize(next stdapi.HandlerFunc) stdapi.HandlerFunc {
return func(c *stdapi.Context) error {
switch c.Request().Method {
case http.MethodGet:
if !CanRead(c) {
return stdapi.Errorf(401, "you are unauthorized to access this")
}
default:
if !CanWrite(c) {
return stdapi.Errorf(401, "you are unauthorized to access this")
}
}
return next(c)
}
}

func CanRead(c *stdapi.Context) bool {
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(structs.ConvoxRoleParam); d != nil {
v, _ := d.(string)
return strings.Contains(v, "w")
}
return false
}

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

func SetReadWriteRole(c *stdapi.Context) {
c.Set(structs.ConvoxRoleParam, structs.ConvoxRoleReadWrite)
}
63 changes: 63 additions & 0 deletions pkg/api/authorization_test.go
@@ -0,0 +1,63 @@
package api_test

import (
"net/http"
"net/http/httptest"
"testing"

"github.com/convox/rack/pkg/api"
"github.com/convox/stdapi"
"github.com/stretchr/testify/assert"
)

func TestAuthorize(t *testing.T) {
s := &api.Server{}

testData := []struct {
c *stdapi.Context
access bool
}{
{
c: func() *stdapi.Context {
c := stdapi.NewContext(nil, httptest.NewRequest(http.MethodGet, "http://text.com", nil))
api.SetReadRole(c)
return c
}(),
access: true,
},
{
c: func() *stdapi.Context {
c := stdapi.NewContext(nil, httptest.NewRequest(http.MethodGet, "http://text.com", nil))
return c
}(),
access: false,
},
{
c: func() *stdapi.Context {
c := stdapi.NewContext(nil, httptest.NewRequest(http.MethodPost, "http://text.com", nil))
api.SetReadRole(c)
return c
}(),
access: false,
},
{
c: func() *stdapi.Context {
c := stdapi.NewContext(nil, httptest.NewRequest(http.MethodPost, "http://text.com", nil))
api.SetReadWriteRole(c)
return c
}(),
access: true,
},
}

for _, td := range testData {
err := s.Authorize(func(c *stdapi.Context) error {
return nil
})(td.c)
if td.access {
assert.Nil(t, err)
} else {
assert.NotNil(t, err)
}
}
}
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
}
return c.RenderOK()
}

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")
}

var tk string

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

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

func (s *Server) SystemLogs(c *stdapi.Context) error {
if err := s.hook("SystemLogsValidate", c); err != nil {
return err
Expand Down
4 changes: 4 additions & 0 deletions pkg/api/routes.go
Expand Up @@ -3,6 +3,8 @@ package api
import "github.com/convox/stdapi"

func (s *Server) setupRoutes(r stdapi.Router) {
r.Use(s.Authorize)

r.Route("POST", "/apps/{name}/cancel", s.AppCancel)
r.Route("POST", "/apps", s.AppCreate)
r.Route("DELETE", "/apps/{name}", s.AppDelete)
Expand Down Expand Up @@ -60,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-in-hour", "", "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
}

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

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

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

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

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

return c.OK()
}

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

Expand Down

0 comments on commit 1361189

Please sign in to comment.