omni/internal/backend/workloadproxy/accessvalidator.go
Artem Chernyshev ed946b30a6
feat: display OMNI_ENDPOINT in the service account creation UI
Fixes: https://github.com/siderolabs/omni/issues/858

Signed-off-by: Artem Chernyshev <artem.chernyshev@talos-systems.com>
2025-01-29 15:27:36 +03:00

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)
}