mirror of
https://tangled.org/evan.jarrett.net/at-container-registry
synced 2026-08-28 20:06:02 +00:00
fix issue with mismatched scopes locally
This commit is contained in:
+2
-1
@@ -39,7 +39,8 @@ LABEL org.opencontainers.image.title="ATCR AppView" \
|
||||
org.opencontainers.image.documentation="https://tangled.org/@evan.jarrett.net/at-container-registry" \
|
||||
org.opencontainers.image.licenses="MIT" \
|
||||
org.opencontainers.image.version="0.1.0" \
|
||||
io.atcr.icon="https://imgs.blue/evan.jarrett.net/1TpTNrRelfloN2emuWZDrWmPT0o93bAjEnozjD6UPgoVV9m4"
|
||||
io.atcr.icon="https://imgs.blue/evan.jarrett.net/1TpTNrRelfloN2emuWZDrWmPT0o93bAjEnozjD6UPgoVV9m4" \
|
||||
io.atcr.readme="https://tangled.org/@evan.jarrett.net/at-container-registry/raw/main/docs/appview.md"
|
||||
|
||||
ENTRYPOINT ["/atcr-appview"]
|
||||
CMD ["serve"]
|
||||
|
||||
+13
-9
@@ -154,16 +154,26 @@ func serveRegistry(cmd *cobra.Command, args []string) error {
|
||||
// The extraction function normalizes URLs to DIDs for consistency
|
||||
defaultHoldDID := appview.ExtractDefaultHoldDID(config)
|
||||
|
||||
// Extract test mode from config (needed for OAuth scope configuration)
|
||||
testMode := appview.ExtractTestMode(config)
|
||||
if testMode {
|
||||
fmt.Println("TEST_MODE enabled - will use HTTP for local DID resolution and transition:generic scope")
|
||||
}
|
||||
|
||||
// Create OAuth app (indigo client)
|
||||
oauthApp, err := oauth.NewApp(baseURL, oauthStore, defaultHoldDID)
|
||||
oauthApp, err := oauth.NewApp(baseURL, oauthStore, defaultHoldDID, testMode)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create OAuth app: %w", err)
|
||||
}
|
||||
fmt.Println("Using full OAuth scopes (including blob: scope)")
|
||||
if testMode {
|
||||
fmt.Println("Using OAuth scopes with transition:generic (test mode)")
|
||||
} else {
|
||||
fmt.Println("Using OAuth scopes with RPC scope (production mode)")
|
||||
}
|
||||
|
||||
// Invalidate sessions with mismatched scopes on startup
|
||||
// This ensures all users have the latest required scopes after deployment
|
||||
desiredScopes := oauth.GetDefaultScopes(defaultHoldDID)
|
||||
desiredScopes := oauth.GetDefaultScopes(defaultHoldDID, testMode)
|
||||
invalidatedCount, err := oauthStore.InvalidateSessionsWithMismatchedScopes(context.Background(), desiredScopes)
|
||||
if err != nil {
|
||||
fmt.Printf("Warning: Failed to invalidate sessions with mismatched scopes: %v\n", err)
|
||||
@@ -186,12 +196,6 @@ func serveRegistry(cmd *cobra.Command, args []string) error {
|
||||
metricsDB := db.NewMetricsDB(uiDatabase)
|
||||
middleware.SetGlobalDatabase(metricsDB)
|
||||
|
||||
// Extract test mode from config
|
||||
testMode := appview.ExtractTestMode(config)
|
||||
if testMode {
|
||||
fmt.Println("TEST_MODE enabled - will use HTTP for local DID resolution")
|
||||
}
|
||||
|
||||
// Create RemoteHoldAuthorizer for hold authorization with caching
|
||||
holdAuthorizer := auth.NewRemoteHoldAuthorizer(uiDatabase, testMode)
|
||||
middleware.SetGlobalAuthorizer(holdAuthorizer)
|
||||
|
||||
+104
@@ -0,0 +1,104 @@
|
||||
# ATCR AppView
|
||||
|
||||
The **AppView** is the OCI-compliant registry server for ATCR (ATProto Container Registry). It provides the Docker Registry HTTP API V2 and a web interface for browsing container images.
|
||||
|
||||
## What is AppView?
|
||||
|
||||
AppView serves as the central registry server that:
|
||||
|
||||
- **Serves OCI Distribution API** - Compatible with Docker, containerd, podman, and other OCI clients
|
||||
- **Resolves ATProto identities** - Converts handles and DIDs to PDS endpoints
|
||||
- **Routes manifests** - Stores container manifests as ATProto records in users' Personal Data Servers
|
||||
- **Routes blobs** - Proxies blob operations to hold services (S3-compatible storage)
|
||||
- **Provides web UI** - Browse, search, and star repositories
|
||||
|
||||
## Image Format
|
||||
|
||||
Container images use ATProto identities:
|
||||
|
||||
```
|
||||
atcr.io/alice.bsky.social/myapp:latest
|
||||
atcr.io/did:plc:xyz123/myapp:latest
|
||||
```
|
||||
|
||||
## Using ATCR
|
||||
|
||||
### Push Images
|
||||
|
||||
```bash
|
||||
# Install credential helper
|
||||
curl -fsSL https://atcr.io/install.sh | bash
|
||||
|
||||
# Configure Docker (add to ~/.docker/config.json)
|
||||
{
|
||||
"credHelpers": {
|
||||
"atcr.io": "atcr"
|
||||
}
|
||||
}
|
||||
|
||||
# Push images (authenticates automatically)
|
||||
docker tag myapp:latest atcr.io/yourhandle/myapp:latest
|
||||
docker push atcr.io/yourhandle/myapp:latest
|
||||
```
|
||||
|
||||
### Pull Images
|
||||
|
||||
```bash
|
||||
# Public images (no auth required)
|
||||
docker pull atcr.io/alice.bsky.social/myapp:latest
|
||||
|
||||
# Private images (automatic OAuth authentication)
|
||||
docker pull atcr.io/yourhandle/private-app:latest
|
||||
```
|
||||
|
||||
## Running Your Own AppView
|
||||
|
||||
Deploy your own registry instance with Docker Compose:
|
||||
|
||||
```bash
|
||||
# Create configuration
|
||||
cp .env.appview.example .env.appview
|
||||
# Edit .env.appview with your settings
|
||||
|
||||
# Start services
|
||||
docker-compose up -d
|
||||
```
|
||||
|
||||
### Configuration
|
||||
|
||||
Key environment variables:
|
||||
|
||||
- `ATCR_HTTP_ADDR` - HTTP listen address (default: `:5000`)
|
||||
- `ATCR_BASE_URL` - Public URL for OAuth/JWT realm
|
||||
- `ATCR_DEFAULT_HOLD_DID` - Default hold service DID for blob storage (required)
|
||||
- `ATCR_UI_ENABLED` - Enable web interface (default: `true`)
|
||||
- `JETSTREAM_URL` - ATProto event stream URL for real-time updates
|
||||
|
||||
See [deployment documentation](https://tangled.org/@evan.jarrett.net/at-container-registry/blob/main/deploy/README.md) for production setup.
|
||||
|
||||
## Features
|
||||
|
||||
- ✅ **OCI-compliant** - Full Docker Registry API V2 support
|
||||
- ✅ **ATProto OAuth** - Secure authentication with DPoP
|
||||
- ✅ **Decentralized storage** - Manifests stored in users' PDS
|
||||
- ✅ **Web UI** - Browse repositories, view tags, search images
|
||||
- ✅ **Real-time updates** - Jetstream integration for live indexing
|
||||
- ✅ **Multi-arch support** - ARM64, AMD64, and other platforms
|
||||
- ✅ **BYOS** - Bring Your Own Storage via hold services
|
||||
|
||||
## Storage Architecture
|
||||
|
||||
**Hybrid model:**
|
||||
- **Manifests** → ATProto records in user's PDS (small JSON metadata)
|
||||
- **Blobs** → Hold services with S3-compatible backends (large binary layers)
|
||||
|
||||
This design keeps metadata portable and federated while leveraging cheap blob storage for layers.
|
||||
|
||||
## License
|
||||
|
||||
MIT
|
||||
|
||||
---
|
||||
|
||||
**Documentation:** https://tangled.org/@evan.jarrett.net/at-container-registry
|
||||
**Source Code:** https://tangled.org/@evan.jarrett.net/at-container-registry
|
||||
@@ -20,10 +20,12 @@ require (
|
||||
github.com/ipld/go-car v0.6.1-0.20230509095817-92d28eb23ba4
|
||||
github.com/klauspost/compress v1.18.0
|
||||
github.com/mattn/go-sqlite3 v1.14.32
|
||||
github.com/microcosm-cc/bluemonday v1.0.27
|
||||
github.com/multiformats/go-multihash v0.2.3
|
||||
github.com/opencontainers/go-digest v1.0.0
|
||||
github.com/spf13/cobra v1.8.0
|
||||
github.com/whyrusleeping/cbor-gen v0.3.1
|
||||
github.com/yuin/goldmark v1.7.13
|
||||
go.opentelemetry.io/otel v1.32.0
|
||||
go.yaml.in/yaml/v4 v4.0.0-rc.2
|
||||
golang.org/x/crypto v0.39.0
|
||||
@@ -87,7 +89,6 @@ require (
|
||||
github.com/jmespath/go-jmespath v0.4.0 // indirect
|
||||
github.com/klauspost/cpuid/v2 v2.2.7 // indirect
|
||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||
github.com/microcosm-cc/bluemonday v1.0.27 // indirect
|
||||
github.com/minio/sha256-simd v1.0.1 // indirect
|
||||
github.com/mr-tron/base58 v1.2.0 // indirect
|
||||
github.com/multiformats/go-base32 v0.1.0 // indirect
|
||||
@@ -108,7 +109,6 @@ require (
|
||||
github.com/sirupsen/logrus v1.9.3 // indirect
|
||||
github.com/spaolacci/murmur3 v1.1.0 // indirect
|
||||
github.com/spf13/pflag v1.0.5 // indirect
|
||||
github.com/yuin/goldmark v1.7.13 // indirect
|
||||
gitlab.com/yawning/secp256k1-voi v0.0.0-20230925100816-f2616030848b // indirect
|
||||
gitlab.com/yawning/tuplehash v0.0.0-20230713102510-df83abbf9a02 // indirect
|
||||
go.opentelemetry.io/contrib/bridges/prometheus v0.57.0 // indirect
|
||||
|
||||
@@ -29,7 +29,7 @@ func NewFetcher() *Fetcher {
|
||||
// Configure markdown renderer with GitHub-flavored markdown
|
||||
md := goldmark.New(
|
||||
goldmark.WithExtensions(
|
||||
extension.GFM, // GitHub Flavored Markdown
|
||||
extension.GFM, // GitHub Flavored Markdown
|
||||
extension.Typographer, // Smart quotes, dashes, etc.
|
||||
),
|
||||
goldmark.WithParserOptions(
|
||||
|
||||
@@ -20,8 +20,8 @@ type App struct {
|
||||
}
|
||||
|
||||
// NewApp creates a new OAuth app for ATCR with default scopes
|
||||
func NewApp(baseURL string, store oauth.ClientAuthStore, holdDid string) (*App, error) {
|
||||
return NewAppWithScopes(baseURL, store, GetDefaultScopes(holdDid))
|
||||
func NewApp(baseURL string, store oauth.ClientAuthStore, holdDid string, testMode bool) (*App, error) {
|
||||
return NewAppWithScopes(baseURL, store, GetDefaultScopes(holdDid, testMode))
|
||||
}
|
||||
|
||||
// NewAppWithScopes creates a new OAuth app for ATCR with custom scopes
|
||||
@@ -120,10 +120,10 @@ func RedirectURI(baseURL string) string {
|
||||
}
|
||||
|
||||
// GetDefaultScopes returns the default OAuth scopes for ATCR registry operations
|
||||
func GetDefaultScopes(did string) []string {
|
||||
return []string{
|
||||
// testMode determines whether to use transition:generic (test) or rpc scopes (production)
|
||||
func GetDefaultScopes(did string, testMode bool) []string {
|
||||
scopes := []string{
|
||||
"atproto",
|
||||
"transition:generic",
|
||||
// Image manifest types (single-arch)
|
||||
"blob:application/vnd.oci.image.manifest.v1+json",
|
||||
"blob:application/vnd.docker.distribution.manifest.v2+json",
|
||||
@@ -132,12 +132,25 @@ func GetDefaultScopes(did string) []string {
|
||||
"blob:application/vnd.docker.distribution.manifest.list.v2+json",
|
||||
// OCI artifact manifests (for cosign signatures, SBOMs, attestations)
|
||||
"blob:application/vnd.cncf.oras.artifact.manifest.v1+json",
|
||||
fmt.Sprintf("rpc:com.atproto.repo.getRecord?aud=%s#atcr_hold", did),
|
||||
}
|
||||
|
||||
// In test mode: use transition:generic (local dev with test PDS)
|
||||
// In production: use rpc scope for service auth
|
||||
if testMode {
|
||||
scopes = append(scopes, "transition:generic")
|
||||
} else {
|
||||
scopes = append(scopes, fmt.Sprintf("rpc:com.atproto.repo.getRecord?aud=%s#atcr_hold", did))
|
||||
}
|
||||
|
||||
// Add repo scopes
|
||||
scopes = append(scopes,
|
||||
fmt.Sprintf("repo:%s", atproto.ManifestCollection),
|
||||
fmt.Sprintf("repo:%s", atproto.TagCollection),
|
||||
fmt.Sprintf("repo:%s", atproto.StarCollection),
|
||||
fmt.Sprintf("repo:%s", atproto.SailorProfileCollection),
|
||||
}
|
||||
)
|
||||
|
||||
return scopes
|
||||
}
|
||||
|
||||
// ScopesMatch checks if two scope lists are equivalent (order-independent)
|
||||
|
||||
@@ -33,11 +33,13 @@ func InteractiveFlowWithCallback(
|
||||
}
|
||||
|
||||
// Create OAuth app with custom scopes (or defaults if nil)
|
||||
// Interactive flows are typically for production use (credential helper, etc.)
|
||||
// so we default to testMode=false
|
||||
var app *App
|
||||
if scopes != nil {
|
||||
app, err = NewAppWithScopes(baseURL, store, scopes)
|
||||
} else {
|
||||
app, err = NewApp(baseURL, store, "*")
|
||||
app, err = NewApp(baseURL, store, "*", false)
|
||||
}
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create OAuth app: %w", err)
|
||||
|
||||
Reference in New Issue
Block a user