You've already forked ionscale
mirror of
https://github.com/jsiebens/ionscale.git
synced 2026-04-05 12:32:58 +01:00
feat: login as system admin using oidc
This commit is contained in:
@@ -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 {
|
||||
|
||||
@@ -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{},
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
Reference in New Issue
Block a user