Files
wehub-resource-sync 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
chore: import upstream snapshot with attribution
2026-07-13 12:02:24 +08:00

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
}