-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: initial stab at service-api server implementation
- Loading branch information
Showing
15 changed files
with
1,269 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 ./... |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
FROM alpine:3.14 | ||
ENTRYPOINT ["/service-api"] | ||
COPY service-api / |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
FROM alpine:3.14 | ||
ENTRYPOINT ["/ssh-portal"] | ||
COPY ssh-portal / |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
) |
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
) |
Oops, something went wrong.