mirror of
https://github.com/siderolabs/omni.git
synced 2025-08-08 18:47:01 +02:00
Fixes: https://github.com/siderolabs/omni/issues/858 Signed-off-by: Artem Chernyshev <artem.chernyshev@talos-systems.com>
135 lines
3.9 KiB
Go
135 lines
3.9 KiB
Go
// Copyright (c) 2025 Sidero Labs, Inc.
|
|
//
|
|
// Use of this software is governed by the Business Source License
|
|
// included in the LICENSE file.
|
|
|
|
package workloadproxy
|
|
|
|
import (
|
|
"context"
|
|
"encoding/base64"
|
|
"errors"
|
|
|
|
pgpcrypto "github.com/ProtonMail/gopenpgp/v2/crypto"
|
|
"github.com/cosi-project/runtime/pkg/resource"
|
|
"github.com/cosi-project/runtime/pkg/safe"
|
|
"github.com/cosi-project/runtime/pkg/state"
|
|
"github.com/siderolabs/go-api-signature/pkg/pgp"
|
|
"go.uber.org/zap"
|
|
|
|
"github.com/siderolabs/omni/client/pkg/omni/resources"
|
|
authres "github.com/siderolabs/omni/client/pkg/omni/resources/auth"
|
|
"github.com/siderolabs/omni/internal/pkg/auth"
|
|
"github.com/siderolabs/omni/internal/pkg/auth/accesspolicy"
|
|
"github.com/siderolabs/omni/internal/pkg/auth/actor"
|
|
"github.com/siderolabs/omni/internal/pkg/auth/role"
|
|
"github.com/siderolabs/omni/internal/pkg/ctxstore"
|
|
)
|
|
|
|
// RoleProvider provides the current actor's role for a cluster.
|
|
type RoleProvider interface {
|
|
RoleForCluster(ctx context.Context, id resource.ID) (role.Role, error)
|
|
}
|
|
|
|
// AccessPolicyRoleProvider provides the role for a cluster by looking into the context and evaluating access policies.
|
|
type AccessPolicyRoleProvider struct {
|
|
state state.State
|
|
}
|
|
|
|
// NewAccessPolicyRoleProvider creates a new RoleProvider that uses the access policy in the state.
|
|
func NewAccessPolicyRoleProvider(state state.State) (*AccessPolicyRoleProvider, error) {
|
|
if state == nil {
|
|
return nil, errors.New("state is nil")
|
|
}
|
|
|
|
return &AccessPolicyRoleProvider{state: state}, nil
|
|
}
|
|
|
|
// RoleForCluster returns the role of the current user for the given cluster.
|
|
func (a *AccessPolicyRoleProvider) RoleForCluster(ctx context.Context, clusterID resource.ID) (role.Role, error) {
|
|
accessRole, _, err := accesspolicy.RoleForCluster(ctx, clusterID, a.state)
|
|
|
|
return accessRole, err
|
|
}
|
|
|
|
// PGPAccessValidator validates a signature using PGP keys and roles/ACLs.
|
|
type PGPAccessValidator struct {
|
|
logger *zap.Logger
|
|
state state.State
|
|
roleProvider RoleProvider
|
|
}
|
|
|
|
// NewPGPAccessValidator creates a new PGP signature validator.
|
|
func NewPGPAccessValidator(state state.State, roleProvider RoleProvider, logger *zap.Logger) (*PGPAccessValidator, error) {
|
|
if state == nil {
|
|
return nil, errors.New("state is nil")
|
|
}
|
|
|
|
if roleProvider == nil {
|
|
return nil, errors.New("role provider is nil")
|
|
}
|
|
|
|
if logger == nil {
|
|
logger = zap.NewNop()
|
|
}
|
|
|
|
return &PGPAccessValidator{
|
|
logger: logger,
|
|
state: state,
|
|
roleProvider: roleProvider,
|
|
}, nil
|
|
}
|
|
|
|
// ValidateAccess validates the access to an exposed service in the given cluster ID,
|
|
// using the PGP public keys in the Omni database.
|
|
func (p *PGPAccessValidator) ValidateAccess(ctx context.Context, publicKeyID, publicKeyIDSignatureBase64 string, clusterID resource.ID) error {
|
|
singatureBytes, err := base64.StdEncoding.DecodeString(publicKeyIDSignatureBase64)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
ctx = actor.MarkContextAsInternalActor(ctx)
|
|
|
|
publicKey, err := safe.StateGet[*authres.PublicKey](ctx, p.state, authres.NewPublicKey(resources.DefaultNamespace, publicKeyID).Metadata())
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
key, err := pgpcrypto.NewKeyFromArmored(string(publicKey.TypedSpec().Value.GetPublicKey()))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
pgpKey, err := pgp.NewKey(key)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if err = pgpKey.Validate(); err != nil {
|
|
return err
|
|
}
|
|
|
|
if err = pgpKey.Verify([]byte(publicKeyID), singatureBytes); err != nil {
|
|
return err
|
|
}
|
|
|
|
publicKeyRoleStr := publicKey.TypedSpec().Value.GetRole()
|
|
if publicKeyRoleStr != "" {
|
|
publicKeyRole, parseErr := role.Parse(publicKeyRoleStr)
|
|
if parseErr != nil {
|
|
return parseErr
|
|
}
|
|
|
|
ctx = ctxstore.WithValue(ctx, auth.RoleContextKey{Role: publicKeyRole})
|
|
}
|
|
|
|
ctx = ctxstore.WithValue(ctx, auth.IdentityContextKey{Identity: publicKey.TypedSpec().Value.GetIdentity().GetEmail()})
|
|
|
|
accessRole, err := p.roleProvider.RoleForCluster(ctx, clusterID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
return accessRole.Check(role.Reader)
|
|
}
|