feat: login as system admin using oidc

This commit is contained in:
Johan Siebens
2022-08-31 11:21:31 +02:00
parent 3568764ec1
commit 4234c5eed9
8 changed files with 198 additions and 24 deletions
+7
View File
@@ -140,6 +140,13 @@ type AuthProvider struct {
ClientID string `yaml:"client_id"`
ClientSecret string `yaml:"client_secret"`
Scopes []string `yaml:"additional_scopes"`
SystemAdminPolicy SystemAdminPolicy `yaml:"system_admins"`
}
type SystemAdminPolicy struct {
Subs []string `json:"subs,omitempty"`
Emails []string `json:"emails,omitempty"`
Filters []string `json:"filters,omitempty"`
}
func (c *Config) CreateUrl(format string, a ...interface{}) string {
+1
View File
@@ -45,6 +45,7 @@ func migrate(db *gorm.DB, repository domain.Repository) error {
&domain.Tailnet{},
&domain.Account{},
&domain.User{},
&domain.SystemApiKey{},
&domain.ApiKey{},
&domain.AuthKey{},
&domain.Machine{},
+3
View File
@@ -20,6 +20,9 @@ type Repository interface {
ListTailnets(ctx context.Context) ([]Tailnet, error)
DeleteTailnet(ctx context.Context, id uint64) error
SaveSystemApiKey(ctx context.Context, key *SystemApiKey) error
LoadSystemApiKey(ctx context.Context, key string) (*SystemApiKey, error)
SaveApiKey(ctx context.Context, key *ApiKey) error
LoadApiKey(ctx context.Context, key string) (*ApiKey, error)
DeleteApiKeysByTailnet(ctx context.Context, tailnetID uint64) error
+91
View File
@@ -0,0 +1,91 @@
package domain
import (
"context"
"errors"
"fmt"
"github.com/jsiebens/ionscale/internal/util"
"golang.org/x/crypto/bcrypt"
"gorm.io/gorm"
"strings"
"time"
)
func CreateSystemApiKey(account *Account, expiresAt *time.Time) (string, *SystemApiKey) {
key := util.RandStringBytes(12)
pwd := util.RandStringBytes(22)
value := fmt.Sprintf("sk_%s_%s", key, pwd)
hash, err := bcrypt.GenerateFromPassword([]byte(pwd), bcrypt.DefaultCost)
if err != nil {
panic(err)
}
return value, &SystemApiKey{
ID: util.NextID(),
Key: key,
Hash: string(hash),
CreatedAt: time.Now().UTC(),
ExpiresAt: expiresAt,
AccountID: account.ID,
}
}
type SystemApiKey struct {
ID uint64 `gorm:"primary_key;autoIncrement:false"`
Key string `gorm:"type:varchar(64);unique_index"`
Hash string
CreatedAt time.Time
ExpiresAt *time.Time
AccountID uint64
Account Account
}
func (r *repository) SaveSystemApiKey(ctx context.Context, key *SystemApiKey) error {
tx := r.withContext(ctx).Save(key)
if tx.Error != nil {
return tx.Error
}
return nil
}
func (r *repository) LoadSystemApiKey(ctx context.Context, token string) (*SystemApiKey, error) {
split := strings.Split(token, "_")
if len(split) != 3 {
return nil, nil
}
prefix := split[0]
key := split[1]
value := split[2]
if prefix != "sk" {
return nil, nil
}
var m SystemApiKey
tx := r.withContext(ctx).Preload("Account").First(&m, "key = ?", key)
if errors.Is(tx.Error, gorm.ErrRecordNotFound) {
return nil, nil
}
if tx.Error != nil {
return nil, tx.Error
}
if err := bcrypt.CompareHashAndPassword([]byte(m.Hash), []byte(value)); err != nil {
return nil, nil
}
if !m.ExpiresAt.IsZero() && m.ExpiresAt.Before(time.Now()) {
return nil, nil
}
return &m, nil
}
+44 -9
View File
@@ -22,11 +22,14 @@ import (
func NewAuthenticationHandlers(
config *config.Config,
authProvider provider.AuthProvider,
systemIAMPolicy *domain.IAMPolicy,
repository domain.Repository) *AuthenticationHandlers {
return &AuthenticationHandlers{
config: config,
authProvider: authProvider,
repository: repository,
systemIAMPolicy: systemIAMPolicy,
pendingOAuthUsers: cache.New(5*time.Minute, 10*time.Minute),
}
}
@@ -35,6 +38,7 @@ type AuthenticationHandlers struct {
repository domain.Repository
authProvider provider.AuthProvider
config *config.Config
systemIAMPolicy *domain.IAMPolicy
pendingOAuthUsers *cache.Cache
}
@@ -44,6 +48,7 @@ type AuthFormData struct {
type TailnetSelectionData struct {
Tailnets []domain.Tailnet
SystemAdmin bool
}
type oauthState struct {
@@ -128,12 +133,17 @@ func (h *AuthenticationHandlers) Callback(c echo.Context) error {
return err
}
isSystemAdmin, err := h.isSystemAdmin(ctx, user)
if err != nil {
return err
}
tailnets, err := h.listAvailableTailnets(ctx, user)
if err != nil {
return err
}
if len(tailnets) == 0 {
if !isSystemAdmin && len(tailnets) == 0 {
if state.Flow == "r" {
req, err := h.repository.GetRegistrationRequestByKey(ctx, state.Key)
if err == nil && req != nil {
@@ -157,7 +167,11 @@ func (h *AuthenticationHandlers) Callback(c echo.Context) error {
h.pendingOAuthUsers.Set(state.Key, account, cache.DefaultExpiration)
return c.Render(http.StatusOK, "tailnets.html", &TailnetSelectionData{Tailnets: tailnets})
return c.Render(http.StatusOK, "tailnets.html", &TailnetSelectionData{Tailnets: tailnets, SystemAdmin: isSystemAdmin})
}
func (h *AuthenticationHandlers) isSystemAdmin(ctx context.Context, u *provider.User) (bool, error) {
return h.systemIAMPolicy.EvaluatePolicy(&domain.Identity{UserID: u.ID, Email: u.Name, Attr: u.Attr})
}
func (h *AuthenticationHandlers) listAvailableTailnets(ctx context.Context, u *provider.User) ([]domain.Tailnet, error) {
@@ -221,6 +235,34 @@ func (h *AuthenticationHandlers) Error(c echo.Context) error {
func (h *AuthenticationHandlers) endCliAuthenticationFlow(c echo.Context, req *domain.AuthenticationRequest, state *oauthState) error {
ctx := c.Request().Context()
item, ok := h.pendingOAuthUsers.Get(state.Key)
if !ok {
return c.Redirect(http.StatusFound, "/a/error")
}
oa := item.(*domain.Account)
// continue as system admin?
if c.FormValue("a") == "true" {
expiresAt := time.Now().Add(24 * time.Hour)
token, apiKey := domain.CreateSystemApiKey(oa, &expiresAt)
req.Token = token
err := h.repository.Transaction(func(rp domain.Repository) error {
if err := rp.SaveSystemApiKey(ctx, apiKey); err != nil {
return err
}
if err := rp.SaveAuthenticationRequest(ctx, req); err != nil {
return err
}
return nil
})
if err != nil {
return err
}
return c.Redirect(http.StatusFound, "/a/success")
}
tailnetIDParam := c.FormValue("s")
parseUint, err := strconv.ParseUint(tailnetIDParam, 10, 64)
@@ -232,13 +274,6 @@ func (h *AuthenticationHandlers) endCliAuthenticationFlow(c echo.Context, req *d
return err
}
item, ok := h.pendingOAuthUsers.Get(state.Key)
if !ok {
return c.Redirect(http.StatusFound, "/a/error")
}
oa := item.(*domain.Account)
user, _, err := h.repository.GetOrCreateUserWithAccount(ctx, tailnet, oa)
if err != nil {
return err
+16 -4
View File
@@ -10,6 +10,7 @@ import (
"github.com/jsiebens/ionscale/internal/broker"
"github.com/jsiebens/ionscale/internal/config"
"github.com/jsiebens/ionscale/internal/database"
"github.com/jsiebens/ionscale/internal/domain"
"github.com/jsiebens/ionscale/internal/handlers"
"github.com/jsiebens/ionscale/internal/provider"
"github.com/jsiebens/ionscale/internal/service"
@@ -83,7 +84,7 @@ func Start(c *config.Config) error {
return e
}
authProvider, err := setupAuthProvider(c.AuthProvider)
authProvider, systemIAMPolicy, err := setupAuthProvider(c.AuthProvider)
if err != nil {
return err
}
@@ -94,6 +95,7 @@ func Start(c *config.Config) error {
authenticationHandlers := handlers.NewAuthenticationHandlers(
c,
authProvider,
systemIAMPolicy,
repository,
)
@@ -176,11 +178,21 @@ func Start(c *config.Config) error {
return g.Wait()
}
func setupAuthProvider(config config.AuthProvider) (provider.AuthProvider, error) {
func setupAuthProvider(config config.AuthProvider) (provider.AuthProvider, *domain.IAMPolicy, error) {
if len(config.Issuer) == 0 {
return nil, nil
return nil, &domain.IAMPolicy{}, nil
}
return provider.NewOIDCProvider(&config)
authProvider, err := provider.NewOIDCProvider(&config)
if err != nil {
return nil, nil, err
}
return authProvider, &domain.IAMPolicy{
Subs: config.SystemAdminPolicy.Subs,
Emails: config.SystemAdminPolicy.Emails,
Filters: config.SystemAdminPolicy.Filters,
}, nil
}
func metricsListener(config *config.Config) (net.Listener, error) {
+9 -4
View File
@@ -82,13 +82,18 @@ func exchangeToken(ctx context.Context, systemAdminKey key.ServerPrivate, reposi
}
apiKey, err := repository.LoadApiKey(ctx, value)
if err != nil || apiKey == nil {
return nil
}
if err == nil && apiKey != nil {
user := apiKey.User
tailnet := apiKey.Tailnet
role := tailnet.IAMPolicy.GetRole(user)
return &Principal{User: &apiKey.User, SystemRole: domain.SystemRoleNone, UserRole: role}
}
systemApiKey, err := repository.LoadSystemApiKey(ctx, value)
if err == nil && systemApiKey != nil {
return &Principal{SystemRole: domain.SystemRoleAdmin}
}
return nil
}
+20
View File
@@ -74,10 +74,29 @@
</head>
<body>
<div class="wrapper">
{{if .SystemAdmin}}
<div style="text-align: left; padding-bottom: 10px">
<p><b>System Admin</b></p>
<small>You are a member of the System Admin group:</small>
</div>
<form method="post">
<ul class="selectionList">
<li><button type="submit" name="a" value="true">OK, continue as System Admin</button></li>
</ul>
</form>
{{end}}
{{if .Tailnets}}
{{if .SystemAdmin}}
<div style="text-align: left; padding-bottom: 10px; padding-top: 20px">
<small>Or select your <b>tailnet</b>:</small>
</div>
{{end}}
{{if not .SystemAdmin}}
<div style="text-align: left; padding-bottom: 10px;">
<p><b>Tailnets</b></p>
<small>Select your tailnet:</small>
</div>
{{end}}
<form method="post">
<ul class="selectionList">
{{range .Tailnets}}
@@ -85,6 +104,7 @@
{{end}}
</ul>
</form>
{{end}}
</div>
</body>
</html>