4cddfcf2f3
Backend Tests / Tests (other) (push) Failing after 1s
Backend Tests / Static Checks (push) Failing after 2s
Backend Tests / Tests (internal) (push) Failing after 2s
Backend Tests / Tests (server) (push) Failing after 1s
Backend Tests / Tests (store) (push) Failing after 1s
Build Canary Image / build-frontend (push) Failing after 1s
Frontend Tests / Lint (push) Failing after 1s
Build Canary Image / build-push (linux/amd64) (push) Has been skipped
Build Canary Image / build-push (linux/arm64) (push) Has been skipped
Build Canary Image / merge (push) Has been skipped
Frontend Tests / Build (push) Failing after 1s
Release Please / release-please (push) Failing after 0s
Proto Linter / Lint Protos (push) Failing after 2s
166 lines
5.5 KiB
Go
166 lines
5.5 KiB
Go
package store
|
|
|
|
import (
|
|
"context"
|
|
"io"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
|
|
"github.com/pkg/errors"
|
|
"google.golang.org/protobuf/encoding/protojson"
|
|
|
|
"github.com/usememos/memos/internal/base"
|
|
storepb "github.com/usememos/memos/proto/gen/store"
|
|
)
|
|
|
|
const (
|
|
maxIdentityProviderBootstrapSize = 1 << 20
|
|
defaultIdentityProviderBootstrapDir = "/etc/secrets"
|
|
identityProviderBootstrapPrefix = "memos-idp-"
|
|
identityProviderBootstrapSuffix = ".json"
|
|
)
|
|
|
|
// ApplyIdentityProviderBootstrapDir validates and reconciles identity providers
|
|
// from memos-idp-*.json files in a directory. Providers not named in those files
|
|
// are left unchanged.
|
|
func (s *Store) ApplyIdentityProviderBootstrapDir(ctx context.Context, dir string) error {
|
|
entries, err := os.ReadDir(dir)
|
|
if err != nil {
|
|
if os.IsNotExist(err) {
|
|
return nil
|
|
}
|
|
return errors.Wrap(err, "failed to read identity provider bootstrap directory")
|
|
}
|
|
|
|
providers := []*storepb.IdentityProvider{}
|
|
for _, entry := range entries {
|
|
if entry.IsDir() || !strings.HasPrefix(entry.Name(), identityProviderBootstrapPrefix) || !strings.HasSuffix(entry.Name(), identityProviderBootstrapSuffix) {
|
|
continue
|
|
}
|
|
provider, err := readIdentityProviderBootstrap(filepath.Join(dir, entry.Name()))
|
|
if err != nil {
|
|
return errors.Wrapf(err, "invalid identity provider bootstrap file %q", entry.Name())
|
|
}
|
|
providers = append(providers, provider)
|
|
}
|
|
if err := validateIdentityProviderBootstrap(providers); err != nil {
|
|
return err
|
|
}
|
|
|
|
for _, provider := range providers {
|
|
existing, err := s.GetIdentityProvider(ctx, &FindIdentityProvider{UID: &provider.Uid})
|
|
if err != nil {
|
|
return errors.Wrapf(err, "failed to look up identity provider %q", provider.Uid)
|
|
}
|
|
if existing == nil {
|
|
if _, err := s.CreateIdentityProvider(ctx, provider); err != nil {
|
|
return errors.Wrapf(err, "failed to create identity provider %q", provider.Uid)
|
|
}
|
|
continue
|
|
}
|
|
if existing.Type != provider.Type {
|
|
return errors.Errorf("identity provider %q has type %s, expected %s", provider.Uid, existing.Type, provider.Type)
|
|
}
|
|
|
|
title := provider.Name
|
|
identifierFilter := provider.IdentifierFilter
|
|
if _, err := s.UpdateIdentityProvider(ctx, &UpdateIdentityProviderV1{
|
|
ID: existing.Id,
|
|
Type: existing.Type,
|
|
Name: &title,
|
|
IdentifierFilter: &identifierFilter,
|
|
Config: provider.Config,
|
|
}); err != nil {
|
|
return errors.Wrapf(err, "failed to update identity provider %q", provider.Uid)
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func readIdentityProviderBootstrap(path string) (*storepb.IdentityProvider, error) {
|
|
file, err := os.Open(path)
|
|
if err != nil {
|
|
return nil, errors.Wrap(err, "failed to read identity provider bootstrap file")
|
|
}
|
|
defer file.Close()
|
|
|
|
content, err := io.ReadAll(io.LimitReader(file, maxIdentityProviderBootstrapSize+1))
|
|
if err != nil {
|
|
return nil, errors.Wrap(err, "failed to read identity provider bootstrap file")
|
|
}
|
|
if len(content) > maxIdentityProviderBootstrapSize {
|
|
return nil, errors.Errorf("identity provider bootstrap file exceeds %d bytes", maxIdentityProviderBootstrapSize)
|
|
}
|
|
|
|
provider := &storepb.IdentityProvider{}
|
|
if err := protojson.Unmarshal(content, provider); err != nil {
|
|
return nil, errors.Wrap(err, "failed to decode identity provider bootstrap file")
|
|
}
|
|
return provider, nil
|
|
}
|
|
|
|
func validateIdentityProviderBootstrap(providers []*storepb.IdentityProvider) error {
|
|
seenUIDs := make(map[string]struct{}, len(providers))
|
|
for i, provider := range providers {
|
|
if provider.Id != 0 {
|
|
return errors.Errorf("identityProviders[%d].id must be omitted", i)
|
|
}
|
|
if !base.UIDMatcher.MatchString(provider.Uid) {
|
|
return errors.Errorf("identityProviders[%d].uid is invalid", i)
|
|
}
|
|
if _, exists := seenUIDs[provider.Uid]; exists {
|
|
return errors.Errorf("identityProviders[%d].uid duplicates %q", i, provider.Uid)
|
|
}
|
|
seenUIDs[provider.Uid] = struct{}{}
|
|
if strings.TrimSpace(provider.Name) == "" {
|
|
return errors.Errorf("identityProviders[%d].name is required", i)
|
|
}
|
|
if provider.Type != storepb.IdentityProvider_OAUTH2 {
|
|
return errors.Errorf("identityProviders[%d].type must be OAUTH2", i)
|
|
}
|
|
if err := validateBootstrapOAuth2Config(provider.Config.GetOauth2Config(), i); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func validateBootstrapOAuth2Config(config *storepb.OAuth2Config, index int) error {
|
|
if config == nil {
|
|
return errors.Errorf("identityProviders[%d].config.oauth2Config is required", index)
|
|
}
|
|
required := []struct {
|
|
name string
|
|
value string
|
|
}{
|
|
{name: "clientId", value: config.ClientId},
|
|
{name: "clientSecret", value: config.ClientSecret},
|
|
{name: "authUrl", value: config.AuthUrl},
|
|
{name: "tokenUrl", value: config.TokenUrl},
|
|
{name: "userInfoUrl", value: config.UserInfoUrl},
|
|
}
|
|
if config.FieldMapping == nil {
|
|
return errors.Errorf("identityProviders[%d].config.oauth2Config.fieldMapping is required", index)
|
|
}
|
|
required = append(required, struct {
|
|
name string
|
|
value string
|
|
}{name: "fieldMapping.identifier", value: config.FieldMapping.Identifier})
|
|
for _, field := range required {
|
|
if strings.TrimSpace(field.value) == "" {
|
|
return errors.Errorf("identityProviders[%d].config.oauth2Config.%s is required", index, field.name)
|
|
}
|
|
}
|
|
if len(config.Scopes) == 0 {
|
|
return errors.Errorf("identityProviders[%d].config.oauth2Config.scopes is required", index)
|
|
}
|
|
for scopeIndex, scope := range config.Scopes {
|
|
if strings.TrimSpace(scope) == "" {
|
|
return errors.Errorf("identityProviders[%d].config.oauth2Config.scopes[%d] must not be empty", index, scopeIndex)
|
|
}
|
|
}
|
|
return nil
|
|
}
|