Skip to content

Commit

Permalink
feat: initial stab at service-api server implementation
Browse files Browse the repository at this point in the history
  • Loading branch information
smlx committed Nov 17, 2021
1 parent 83519f9 commit 4fcf622
Show file tree
Hide file tree
Showing 15 changed files with 1,269 additions and 1 deletion.
13 changes: 13 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
# local development targets

.PHONY: test
test: mod-tidy generate
go test -v ./...

.PHONY: mod-tidy
mod-tidy: generate
go mod tidy

.PHONY: generate
generate:
go generate ./...
61 changes: 60 additions & 1 deletion cmd/service-api/serve.go
Original file line number Diff line number Diff line change
@@ -1,4 +1,63 @@
package main

import (
"context"
"fmt"
"os"
"os/signal"

"github.com/uselagoon/ssh-portal/internal/keycloak"
"github.com/uselagoon/ssh-portal/internal/lagoondb"
"github.com/uselagoon/ssh-portal/internal/serviceapi"
"go.uber.org/zap"

_ "github.com/go-sql-driver/mysql"
)

// ServeCmd represents the serve command.
type ServeCmd struct{}
type ServeCmd struct {
NATSServer string `kong:"required,help='NATS server URL (nats://... or tls://...)'"`
APIDB string `kong:"required,help='Lagoon API Database DSN (https://github.com/go-sql-driver/mysql#dsn-data-source-name)'"`
JWTSecret string `kong:"required,help='JWT Symmetric Secret'"`
KeycloakBaseURL string `kong:"required,help='Keycloak Base URL'"`
KeycloakClientID string `kong:"default='service-api',help='Keycloak OAuth2 Client ID'"`
KeycloakClientSecret string `kong:"required,help='Keycloak OAuth2 Client Secret'"`
}

// getContext starts a goroutine to handle ^C gracefully, and returns a context
// with a "cancel" function which cleans up the signal handling and ensures the
// goroutine exits. This "cancel" function should be deferred in Run().
func getContext() (context.Context, func()) {
ctx, cancel := context.WithCancel(context.Background())
signalChan := make(chan os.Signal, 1)
signal.Notify(signalChan, os.Interrupt)
go func() {
select {
case <-signalChan:
cancel()
case <-ctx.Done():
}
<-signalChan
os.Exit(130) // https://tldp.org/LDP/abs/html/exitcodes.html
}()
return ctx, func() { signal.Stop(signalChan); cancel() }
}

// Run the serve command to service API requests.
func (cmd *ServeCmd) Run(log *zap.Logger) error {
ctx, cancel := getContext()
defer cancel()
// init lagoon DB client
l, err := lagoondb.NewClient(ctx, cmd.APIDB)
if err != nil {
return fmt.Errorf("couldn't init lagoon DBClient: %v", err)
}
// init keycloak client
k, err := keycloak.NewClient(ctx, log, cmd.KeycloakBaseURL,
cmd.KeycloakClientID, cmd.KeycloakClientSecret, cmd.JWTSecret)
if err != nil {
return fmt.Errorf("couldn't init keycloak Client: %v", err)
}
// start serving NATS requests
return serviceapi.ServeNATS(ctx, log, l, k, cmd.NATSServer)
}
3 changes: 3 additions & 0 deletions deploy/service-api/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
FROM alpine:3.14
ENTRYPOINT ["/service-api"]
COPY service-api /
3 changes: 3 additions & 0 deletions deploy/ssh-portal/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
FROM alpine:3.14
ENTRYPOINT ["/ssh-portal"]
COPY ssh-portal /
14 changes: 14 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,25 @@ go 1.17

require (
github.com/alecthomas/kong v0.2.18
github.com/go-sql-driver/mysql v1.6.0
github.com/google/uuid v1.3.0
github.com/jmoiron/sqlx v1.3.4
github.com/nats-io/nats.go v1.13.1-0.20211018182449-f2416a8b1483
go.uber.org/zap v1.19.1
golang.org/x/oauth2 v0.0.0-20211104180415-d3ed0bb246c8
gopkg.in/square/go-jose.v2 v2.6.0
)

require (
github.com/golang/protobuf v1.5.2 // indirect
github.com/nats-io/nats-server/v2 v2.6.4 // indirect
github.com/nats-io/nkeys v0.3.0 // indirect
github.com/nats-io/nuid v1.0.1 // indirect
github.com/pkg/errors v0.9.1 // indirect
go.uber.org/atomic v1.7.0 // indirect
go.uber.org/multierr v1.6.0 // indirect
golang.org/x/crypto v0.0.0-20210616213533-5ff15b29337e // indirect
golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4 // indirect
google.golang.org/appengine v1.6.6 // indirect
google.golang.org/protobuf v1.27.1 // indirect
)
380 changes: 380 additions & 0 deletions go.sum

Large diffs are not rendered by default.

97 changes: 97 additions & 0 deletions internal/keycloak/client.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
package keycloak

import (
"context"
"fmt"
"net/http"
"net/url"
"path"
"time"

"github.com/google/uuid"
"go.uber.org/zap"
"golang.org/x/oauth2"
"gopkg.in/square/go-jose.v2/jwt"
)

// Client is a keycloak client.
type Client struct {
ctx context.Context
baseURL *url.URL
clientID string
clientSecret string
jwtSecret string
log *zap.Logger
}

// realmAccess is a helper struct for json unmarshalling
type realmAccess struct {
Roles []string `json:"roles"`
}

// attributes injected into the access token by keycloak
type userAttributes struct {
RealmAccess *realmAccess `json:"realm_access"`
UserGroups []string `json:"groups"`
GroupProjectIDs map[string][]int `json:"group_lagoon_project_ids"`
}

// NewClient creates a new keycloak client.
func NewClient(ctx context.Context, log *zap.Logger, baseURL, clientID,
clientSecret, jwtSecret string) (*Client, error) {
u, err := url.Parse(baseURL)
if err != nil {
return nil, err
}
return &Client{
ctx: ctx,
baseURL: u,
clientID: clientID,
clientSecret: clientSecret,
jwtSecret: jwtSecret,
log: log,
}, nil
}

// UserRolesAndGroups queries Keycloak given the user UUID, and returns the
// user's realm roles, group memberships, and the project IDs associated with
// those groups.
func (c *Client) UserRolesAndGroups(userUUID *uuid.UUID) ([]string, []string,
map[string][]int, error) {
// get user token
tokenURL := c.baseURL
tokenURL.Path = path.Join(tokenURL.Path,
`/auth/realms/lagoon/protocol/openid-connect/token`)
userConfig := oauth2.Config{
ClientID: c.clientID,
ClientSecret: c.clientSecret,
Endpoint: oauth2.Endpoint{
TokenURL: tokenURL.String(),
},
}
ctx := context.WithValue(c.ctx, oauth2.HTTPClient, &http.Client{
Timeout: 10 * time.Second,
})
userToken, err := userConfig.Exchange(ctx, "",
// https://datatracker.ietf.org/doc/html/rfc8693#section-2.1
oauth2.SetAuthURLParam("grant_type",
"urn:ietf:params:oauth:grant-type:token-exchange"),
// https://www.keycloak.org/docs/latest/securing_apps/#_token-exchange
oauth2.SetAuthURLParam("requested_subject", userUUID.String()))
if err != nil {
return nil, nil, nil, fmt.Errorf("couldn't get user token: %v", err)
}
c.log.Debug("got user token", zap.String("access token",
userToken.AccessToken))
// parse and extract verified attributes
tok, err := jwt.ParseSigned(userToken.AccessToken)
if err != nil {
return nil, nil, nil, fmt.Errorf("couldn't parse verified access token: %v", err)
}
var attr userAttributes
if err = tok.Claims(c.jwtSecret, &attr); err != nil {
return nil, nil, nil,
fmt.Errorf("couldn't extract token claims: %v", err)
}
return attr.RealmAccess.Roles, attr.UserGroups, attr.GroupProjectIDs, nil
}
13 changes: 13 additions & 0 deletions internal/lagoon/environmenttype.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package lagoon

//go:generate enumer -type=EnvironmentType -sql -transform=lower

// EnvironmentType is an enum of valid Environment types.
type EnvironmentType int

const (
// Development environment type.
Development EnvironmentType = iota
// Production environment type.
Production
)
109 changes: 109 additions & 0 deletions internal/lagoon/environmenttype_enumer.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

19 changes: 19 additions & 0 deletions internal/lagoon/userrole.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package lagoon

//go:generate enumer -type=UserRole -sql -transform=lower

// UserRole is an enum of valid User roles.
type UserRole int

const (
// Guest user role.
Guest UserRole = iota
// Reporter user role.
Reporter
// Developer user role.
Developer
// Maintainer user role.
Maintainer
// Owner user role.
Owner
)
Loading

0 comments on commit 4fcf622

Please sign in to comment.