mycelium

package module
v0.1.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jul 16, 2026 License: Apache-2.0 Imports: 10 Imported by: 0

README

mycelium-sdk-go

Mycelium SDK for Go backends — decode the x-mycelium-profile header injected by the Mycelium API Gateway and enforce tenant/account/role authorization with a fluent, typed API.

Part of the Mycelium SDK family, alongside mycelium-sdk-py (Python) and mycelium-sdk-js (Node/TypeScript). All three have full parity against the gateway's Rust core::Profile — the same method surface (permitFlags/denyFlags, tenant-wide and admin helpers) and the same wire contract (perm serialized as "read"/"write").

Requirements

Install

# Core (framework-agnostic, includes the net/http adapter)
go get github.com/LepistaBioinformatics/mycelium-sdk-go

# Gin adapter (separate module — pulled only if you import it)
go get github.com/LepistaBioinformatics/mycelium-sdk-go/adapters/gin

# Echo adapter (separate module)
go get github.com/LepistaBioinformatics/mycelium-sdk-go/adapters/echo

The gin and echo adapters live in their own modules, so a service that only uses the core package or the net/http adapter never compiles Gin or Echo.

Core usage

import mycelium "github.com/LepistaBioinformatics/mycelium-sdk-go"

profile, err := mycelium.DecodeAndDecompressProfileFromBase64(headerValue)
if err != nil {
    // handle *mycelium.ProfileDecodingError
}

related, err := profile.
    WithReadAccess().
    OnTenant(tenantID).
    WithRoles([]string{"admin"}).
    OnAccount(accountID).
    GetRelatedAccountOrError()

Every filter method returns a copy — the original Profile is never mutated.

DecodeAndDecompressProfileFromBase64 returns a *ProfileDecodingError on any failure (empty input, invalid Base64, failed decompression, or invalid JSON). By default it is strict; pass WithStrict(false) to fall back to treating the payload as uncompressed (useful in development):

profile, err := mycelium.DecodeAndDecompressProfileFromBase64(headerValue, mycelium.WithStrict(false))

To produce a header value (e.g. in tests or a gateway-like service), use the encode counterpart:

encoded, err := mycelium.CompressAndEncodeProfileToBase64(profile)

net/http usage

import (
    mycelium "github.com/LepistaBioinformatics/mycelium-sdk-go"
    myceliumhttp "github.com/LepistaBioinformatics/mycelium-sdk-go/adapters/nethttp"
)

mux := http.NewServeMux()
mux.HandleFunc("/whoami", func(w http.ResponseWriter, r *http.Request) {
    profile, ok := myceliumhttp.FromContext(r.Context())
    if !ok {
        http.Error(w, "no profile", http.StatusUnauthorized)
        return
    }
    fmt.Fprintf(w, "accId: %s", profile.AccID)
})

handler := myceliumhttp.Middleware()(mux)
http.ListenAndServe(":8080", handler)

In development a missing or invalid header is tolerated (the profile is simply absent from the context). In any other environment a missing header responds 403 and an undecodable header 401. The environment is read from ENVIRONMENT (default development) or set explicitly with myceliumhttp.Middleware(myceliumhttp.WithEnvironment("production")).

Gin usage

import (
    myceliumgin "github.com/LepistaBioinformatics/mycelium-sdk-go/adapters/gin"
    "github.com/gin-gonic/gin"
)

r := gin.New()
r.Use(myceliumgin.Middleware())

r.GET("/whoami", func(c *gin.Context) {
    if profile, ok := myceliumgin.FromContext(c); ok {
        c.JSON(200, gin.H{"accId": profile.AccID})
        return
    }
    c.JSON(401, gin.H{"detail": "no profile"})
})

Echo usage

import (
    myceliumecho "github.com/LepistaBioinformatics/mycelium-sdk-go/adapters/echo"
    "github.com/labstack/echo/v4"
)

e := echo.New()
e.Use(myceliumecho.Middleware())

e.GET("/whoami", func(c echo.Context) error {
    if profile, ok := myceliumecho.FromContext(c); ok {
        return c.JSON(200, map[string]string{"accId": profile.AccID.String()})
    }
    return c.JSON(401, map[string]string{"detail": "no profile"})
})

All three adapters share the same environment-gated behavior (dev-tolerant; 403 on missing and 401 on undecodable headers in production) and expose FromContext.

Error handling

All SDK errors embed *MyceliumError (Message, Code, ExpTrue) and support errors.As:

Error Code Returned by
*ProfileDecodingError MYC00020 DecodeAndDecompressProfileFromBase64 on any decode failure
*InsufficientLicensesError MYC00019 GetRelatedAccountOrError when licensed resources are present but empty
*InsufficientPrivilegesError MYC00019 Any method requiring a privilege the profile lacks (carries FilteringState)
var decErr *mycelium.ProfileDecodingError
if errors.As(err, &decErr) {
    // handle a decode failure specifically
}

License

Apache-2.0

Documentation

Index

Constants

View Source
const (
	// DefaultProfileKey carries the Base64 + ZSTD compressed JSON Profile.
	DefaultProfileKey = "x-mycelium-profile"
	// DefaultEmailKey carries the authenticated user email.
	DefaultEmailKey = "x-mycelium-email"
	// DefaultScopeKey carries the request scope.
	DefaultScopeKey = "x-mycelium-scope"
	// DefaultMyceliumRoleKey carries the request role.
	DefaultMyceliumRoleKey = "x-mycelium-role"
	// DefaultRequestIDKey carries the request correlation id.
	DefaultRequestIDKey = "x-mycelium-request-id"
	// DefaultConnectionStringKey carries the service connection string.
	DefaultConnectionStringKey = "x-mycelium-connection-string"
	// DefaultTenantIDKey carries the resolved tenant id.
	DefaultTenantIDKey = "x-mycelium-tenant-id"
)

Header keys injected by the Mycelium API Gateway into downstream requests.

These mirror the gateway constants in lib/http_tools/src/settings.rs and the sibling SDKs (mycelium-sdk-py, mycelium-sdk-js). Only DefaultProfileKey is consumed by the decode/middleware code; the rest are provided for consumers.

Variables

This section is empty.

Functions

func CompressAndEncodeProfileToBase64

func CompressAndEncodeProfileToBase64(p *Profile) (string, error)

CompressAndEncodeProfileToBase64 mirrors the gateway encode chain: JSON serialize -> ZSTD compress (level 3) -> Base64 STANDARD. The output decodes with DecodeAndDecompressProfileFromBase64 and the gateway alike.

Types

type DecodeOption

type DecodeOption func(*decodeConfig)

DecodeOption configures DecodeAndDecompressProfileFromBase64.

func WithStrict

func WithStrict(strict bool) DecodeOption

WithStrict controls ZSTD failure handling. When strict (the default), a ZSTD decompression failure returns an error. When not strict, the Base64-decoded bytes are treated as already-uncompressed (dev mode where the gateway did not compress the profile).

type InsufficientLicensesError

type InsufficientLicensesError struct {
	*MyceliumError
}

InsufficientLicensesError is raised when a profile has no licensed resources satisfying the requested access.

func NewInsufficientLicensesError

func NewInsufficientLicensesError(message string) *InsufficientLicensesError

NewInsufficientLicensesError builds an InsufficientLicensesError. An empty message falls back to the shared default used by the sibling SDKs.

func (*InsufficientLicensesError) Unwrap

func (e *InsufficientLicensesError) Unwrap() error

Unwrap exposes the embedded base error so errors.As can target *MyceliumError.

type InsufficientPrivilegesError

type InsufficientPrivilegesError struct {
	*MyceliumError
	FilteringState []string
}

InsufficientPrivilegesError is raised when a profile lacks the administrative or scoped privileges required by an operation. It records the filtering state accumulated by the fluent chain for diagnostics.

func NewInsufficientPrivilegesError

func NewInsufficientPrivilegesError(message string, filteringState []string) *InsufficientPrivilegesError

NewInsufficientPrivilegesError builds an InsufficientPrivilegesError with the shared MYC00019 code.

func (*InsufficientPrivilegesError) Unwrap

func (e *InsufficientPrivilegesError) Unwrap() error

Unwrap exposes the embedded base error so errors.As can target *MyceliumError.

type LicensedResource

type LicensedResource struct {
	AccID       uuid.UUID  `json:"accId"`
	SysAcc      bool       `json:"sysAcc"`
	TenantID    uuid.UUID  `json:"tenantId"`
	AccName     string     `json:"accName"`
	Role        string     `json:"role"`
	RoleID      uuid.UUID  `json:"roleId"`
	Perm        Permission `json:"perm"`
	Verified    bool       `json:"verified"`
	PermitFlags *[]string  `json:"permitFlags,omitempty"`
	DenyFlags   *[]string  `json:"denyFlags,omitempty"`
}

LicensedResource is a single guest grant: an account, a role, and the permission held over it within a tenant.

func LicensedResourceFromString

func LicensedResourceFromString(value string) (LicensedResource, error)

LicensedResourceFromString parses the compact URL form produced by String.

func (LicensedResource) String

func (r LicensedResource) String() string

String encodes the resource into the compact URL form used by the gateway's client-facing profile endpoint:

t/{tenantHex}/a/{accHex}/r/{roleHex}?p={role}:{permInt}&s={0|1}&v={0|1}&n={base64(accName)}[&pf=..][&df=..]

UUIDs are hex without hyphens and accName is Base64 STANDARD, byte-matching the Rust LicensedResource ToString impl.

type LicensedResources

type LicensedResources struct {
	Records []LicensedResource
	Urls    []string
}

LicensedResources is a tagged union: either an inline list of records or a list of compact URL strings. On the wire it is exactly one of {"records": [...]} or {"urls": [...]}, mirroring the Rust enum.

func (LicensedResources) MarshalJSON

func (lr LicensedResources) MarshalJSON() ([]byte, error)

MarshalJSON emits the records form when Records is set, otherwise the urls form. An empty value marshals as an empty records list.

func (LicensedResources) ToLicensesVector

func (lr LicensedResources) ToLicensesVector() []LicensedResource

ToLicensesVector returns the materialized records. Records take precedence over urls. URL entries are lazily parsed; entries that fail to parse are skipped (dropping a grant fails safe by reducing access).

func (*LicensedResources) UnmarshalJSON

func (lr *LicensedResources) UnmarshalJSON(data []byte) error

UnmarshalJSON detects the variant by the presence of "records" vs "urls".

type MyceliumError

type MyceliumError struct {
	Message string
	Code    string
	ExpTrue bool
}

MyceliumError is the base error type for the SDK. It carries an optional machine-readable Code and the ExpTrue flag (true for authorization errors that are safe to surface to the caller).

func (*MyceliumError) Error

func (e *MyceliumError) Error() string

type Owner

type Owner struct {
	ID          uuid.UUID `json:"id"`
	Email       string    `json:"email"`
	FirstName   *string   `json:"firstName,omitempty"`
	LastName    *string   `json:"lastName,omitempty"`
	Username    *string   `json:"username,omitempty"`
	IsPrincipal bool      `json:"isPrincipal"`
}

Owner is a principal (or secondary) identity attached to a Profile.

func (Owner) RedactedEmail

func (o Owner) RedactedEmail() string

RedactedEmail returns the owner email with its local part masked.

type Permission

type Permission int

Permission models the gateway's access level. It is backed by an integer (Read=0, Write=1) but the gateway serializes it as the JSON string "read" / "write" in the Records form, while the LicensedResource URL form encodes it as the integer. To stay compatible with the gateway wire format, Permission always MARSHALS to the string form and ACCEPTS both string and integer on unmarshal.

const (
	// PermissionRead grants read access (integer 0).
	PermissionRead Permission = 0
	// PermissionWrite grants write access (integer 1).
	PermissionWrite Permission = 1
)

func PermissionFromInt

func PermissionFromInt(v int) (Permission, error)

PermissionFromInt converts an integer code to a Permission, erroring outside {0, 1}.

func PermissionFromString

func PermissionFromString(s string) (Permission, error)

PermissionFromString converts the wire string form to a Permission, erroring on any value other than "read" / "write".

func (Permission) MarshalJSON

func (p Permission) MarshalJSON() ([]byte, error)

MarshalJSON always emits the string form to match the gateway.

func (Permission) String

func (p Permission) String() string

String returns the wire string form ("read" / "write").

func (Permission) ToInt

func (p Permission) ToInt() int

ToInt returns the integer representation (0 for Read, 1 for Write).

func (*Permission) UnmarshalJSON

func (p *Permission) UnmarshalJSON(data []byte) error

UnmarshalJSON accepts both the string form ("read"/"write") and the integer form (0/1) so a Profile can be decoded from either the Records or the URL representation.

type Profile

type Profile struct {
	Owners             []Owner            `json:"owners"`
	AccID              uuid.UUID          `json:"accId"`
	IsSubscription     bool               `json:"isSubscription"`
	IsStaff            bool               `json:"isStaff"`
	IsManager          bool               `json:"isManager"`
	OwnerIsActive      bool               `json:"ownerIsActive"`
	AccountIsActive    bool               `json:"accountIsActive"`
	AccountWasApproved bool               `json:"accountWasApproved"`
	AccountWasArchived bool               `json:"accountWasArchived"`
	AccountWasDeleted  bool               `json:"accountWasDeleted"`
	VerboseStatus      *VerboseStatus     `json:"verboseStatus,omitempty"`
	LicensedResources  *LicensedResources `json:"licensedResources,omitempty"`
	TenantsOwnership   *TenantsOwnership  `json:"tenantsOwnership,omitempty"`
	Meta               map[string]string  `json:"meta,omitempty"`
	FilteringState     []string           `json:"filteringState,omitempty"`
}

Profile is the authenticated identity context injected by the gateway. Its fluent filter methods return copies (the receiver is never mutated), matching the immutable-chain semantics of the Rust gateway and sibling SDKs.

func DecodeAndDecompressProfileFromBase64

func DecodeAndDecompressProfileFromBase64(input string, opts ...DecodeOption) (*Profile, error)

DecodeAndDecompressProfileFromBase64 reverses the gateway's header encoding: Base64 STANDARD decode -> ZSTD decompress -> JSON deserialize. Any failure returns a *ProfileDecodingError.

func (Profile) GetIdsOrError

func (p Profile) GetIdsOrError() ([]uuid.UUID, error)

GetIdsOrError returns the account ids of the licensed resources. It succeeds when there is at least one id OR the profile has admin privileges.

func (Profile) GetOwnersIds

func (p Profile) GetOwnersIds() []uuid.UUID

GetOwnersIds returns the ids of the profile owners.

func (Profile) GetRelatedAccountOrError

func (p Profile) GetRelatedAccountOrError() (RelatedAccounts, error)

GetRelatedAccountOrError resolves the effective access scope, checking staff and manager privileges before licensed resources.

func (Profile) GetRelatedAccountsOrTenantWidePermissionOrError

func (p Profile) GetRelatedAccountsOrTenantWidePermissionOrError(tenantID uuid.UUID, permission Permission) (RelatedAccounts, error)

GetRelatedAccountsOrTenantWidePermissionOrError tries tenant-wide resolution first, falling back to the account-scoped resolution.

func (Profile) GetTenantWidePermissionOrError

func (p Profile) GetTenantWidePermissionOrError(tenantID uuid.UUID, permission Permission) (RelatedAccounts, error)

GetTenantWidePermissionOrError resolves tenant-wide access: staff, manager, tenant ownership, or tenant-manager role fallback.

func (Profile) HasAdminPrivileges

func (p Profile) HasAdminPrivileges() bool

HasAdminPrivileges reports whether the profile is staff or manager.

func (Profile) HasAdminPrivilegesOrError

func (p Profile) HasAdminPrivilegesOrError() error

HasAdminPrivilegesOrError returns an error when the profile lacks admin privileges.

func (Profile) HasNotDenyFlags

func (p Profile) HasNotDenyFlags(flags []string) Profile

HasNotDenyFlags keeps resources that have NONE of the given flags in their deny_flags (resources with no deny_flags are kept).

func (Profile) HasPermitFlags

func (p Profile) HasPermitFlags(flags []string) Profile

HasPermitFlags keeps resources whose permit_flags contain ALL the given flags.

func (Profile) OnAccount

func (p Profile) OnAccount(accountID uuid.UUID) Profile

OnAccount keeps resources scoped to the given account.

func (Profile) OnTenant

func (p Profile) OnTenant(tenantID uuid.UUID) Profile

OnTenant keeps resources scoped to the given tenant.

func (Profile) OnTenantAsManager

func (p Profile) OnTenantAsManager(tenantID uuid.UUID, permission Permission) Profile

OnTenantAsManager scopes to the tenant with the required permission and the tenant-manager role.

func (Profile) ProfileRedacted

func (p Profile) ProfileRedacted() string

ProfileRedacted returns the profile identifier plus the owner emails with their local parts masked.

func (Profile) ProfileString

func (p Profile) ProfileString() string

ProfileString returns a stable "profile/<accId>" identifier.

func (Profile) WithReadAccess

func (p Profile) WithReadAccess() Profile

WithReadAccess keeps resources granting at least read permission.

func (Profile) WithRoles

func (p Profile) WithRoles(roles []string) Profile

WithRoles keeps resources whose role is in the given list.

func (Profile) WithSystemAccountsAccess

func (p Profile) WithSystemAccountsAccess() Profile

WithSystemAccountsAccess keeps resources belonging to system accounts.

func (Profile) WithTenantOwnershipOrError

func (p Profile) WithTenantOwnershipOrError(tenantID uuid.UUID) (Profile, error)

WithTenantOwnershipOrError returns a filtered profile when it owns the tenant, otherwise an InsufficientPrivilegesError.

func (Profile) WithWriteAccess

func (p Profile) WithWriteAccess() Profile

WithWriteAccess keeps resources granting write permission.

type ProfileDecodingError

type ProfileDecodingError struct {
	*MyceliumError
}

ProfileDecodingError is raised when the x-mycelium-profile header cannot be decoded (invalid Base64, decompression, or JSON).

func NewProfileDecodingError

func NewProfileDecodingError(message string) *ProfileDecodingError

NewProfileDecodingError builds a ProfileDecodingError with the MYC00020 code.

func (*ProfileDecodingError) Unwrap

func (e *ProfileDecodingError) Unwrap() error

Unwrap exposes the embedded base error so errors.As can target *MyceliumError.

type RelatedAccounts

type RelatedAccounts struct {
	Kind     RelatedAccountsKind
	Accounts []uuid.UUID
	TenantID *uuid.UUID
}

RelatedAccounts is the effective access scope resolved from a Profile. It maps to the externally-tagged Rust enum and (un)marshals to the same wire form:

{"allowedAccounts": ["<uuid>", ...]}
{"hasTenantWidePrivileges": "<uuid>"}
"hasStaffPrivileges"
"hasManagerPrivileges"

func NewAllowedAccounts

func NewAllowedAccounts(accounts []uuid.UUID) RelatedAccounts

NewAllowedAccounts builds an AllowedAccounts scope.

func NewHasManagerPrivileges

func NewHasManagerPrivileges() RelatedAccounts

NewHasManagerPrivileges builds a HasManagerPrivileges scope.

func NewHasStaffPrivileges

func NewHasStaffPrivileges() RelatedAccounts

NewHasStaffPrivileges builds a HasStaffPrivileges scope.

func NewHasTenantWidePrivileges

func NewHasTenantWidePrivileges(tenantID uuid.UUID) RelatedAccounts

NewHasTenantWidePrivileges builds a HasTenantWidePrivileges scope.

func (RelatedAccounts) MarshalJSON

func (ra RelatedAccounts) MarshalJSON() ([]byte, error)

MarshalJSON emits the externally-tagged camelCase form matching the gateway.

func (*RelatedAccounts) UnmarshalJSON

func (ra *RelatedAccounts) UnmarshalJSON(data []byte) error

UnmarshalJSON parses the externally-tagged form back into a RelatedAccounts.

type RelatedAccountsKind

type RelatedAccountsKind string

RelatedAccountsKind discriminates the RelatedAccounts variants.

const (
	RelatedAccountsAllowedAccounts         RelatedAccountsKind = "AllowedAccounts"
	RelatedAccountsHasTenantWidePrivileges RelatedAccountsKind = "HasTenantWidePrivileges"
	RelatedAccountsHasStaffPrivileges      RelatedAccountsKind = "HasStaffPrivileges"
	RelatedAccountsHasManagerPrivileges    RelatedAccountsKind = "HasManagerPrivileges"
)

type TenantOwnership

type TenantOwnership struct {
	ID    uuid.UUID `json:"id"`
	Name  string    `json:"name"`
	Since time.Time `json:"since"`
}

TenantOwnership records a tenant a profile owns. Note the JSON keys are id/name/since (NOT camelCased) because the Rust TenantOwnership struct has no rename_all attribute.

func TenantOwnershipFromString

func TenantOwnershipFromString(value string) (TenantOwnership, error)

TenantOwnershipFromString parses the compact URL form produced by String.

func (TenantOwnership) String

func (t TenantOwnership) String() string

String encodes the ownership into the compact URL form tid/{tenantHex}?since={rfc3339}&name={base64(name)}.

type TenantsOwnership

type TenantsOwnership struct {
	Records []TenantOwnership
	Urls    []string
}

TenantsOwnership is a records|urls tagged union of tenant ownerships.

func (TenantsOwnership) MarshalJSON

func (to TenantsOwnership) MarshalJSON() ([]byte, error)

MarshalJSON emits the records form when Records is set, otherwise the urls form.

func (TenantsOwnership) ToOwnershipVector

func (to TenantsOwnership) ToOwnershipVector() []TenantOwnership

ToOwnershipVector returns the materialized ownerships. Records take precedence over urls; unparseable url entries are skipped.

func (*TenantsOwnership) UnmarshalJSON

func (to *TenantsOwnership) UnmarshalJSON(data []byte) error

UnmarshalJSON detects the variant by the presence of "records" vs "urls".

type VerboseStatus

type VerboseStatus string

VerboseStatus is the human-readable account status. It is a plain string on the wire (single lowercase word), so the zero value marshals/unmarshals via the default string codec.

const (
	VerboseStatusUnverified VerboseStatus = "unverified"
	VerboseStatusVerified   VerboseStatus = "verified"
	VerboseStatusInactive   VerboseStatus = "inactive"
	VerboseStatusArchived   VerboseStatus = "archived"
	VerboseStatusDeleted    VerboseStatus = "deleted"
	VerboseStatusUnknown    VerboseStatus = "unknown"
)

func ParseVerboseStatus

func ParseVerboseStatus(s string) VerboseStatus

ParseVerboseStatus returns the matching status, falling back to Unknown for unrecognized values (mirroring the gateway's lenient FromStr).

func (VerboseStatus) IsValid

func (v VerboseStatus) IsValid() bool

IsValid reports whether v is one of the recognized statuses.

Directories

Path Synopsis
adapters
nethttp
Package nethttp provides net/http middleware that extracts the Mycelium profile from the x-mycelium-profile header into the request context.
Package nethttp provides net/http middleware that extracts the Mycelium profile from the x-mycelium-profile header into the request context.
echo module
gin module

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL