ssas

package
v0.0.0-...-95aad33 Latest Latest
Warning

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

Go to latest
Published: Jul 19, 2023 License: CC0-1.0 Imports: 35 Imported by: 0

Documentation

Index

Constants

View Source
const RSAKEYMINBITS = 2048

Variables

View Source
var Connection *gorm.DB
View Source
var CredentialExpiration time.Duration
View Source
var DefaultScope string

Logger provides a structured logger for this service

View Source
var MacaroonExpiration time.Duration
View Source
var MaxIPs int

Functions

func AccessTokenIssued

func AccessTokenIssued(data Event)

AccessTokenIssued should be called to log the successful creation of every access token

func AuthorizationFailure

func AuthorizationFailure(data Event)

AuthorizationFailure should be called by middleware to record token or credential issues

func BlacklistedTokenPresented

func BlacklistedTokenPresented(data Event)

BlacklistedTokenPresented logs an attempt to verify a blacklisted token

func CacheSyncFailure

func CacheSyncFailure(data Event)

CacheSyncFailure is called when an in-memory cache cannot be refreshed from the database

func CleanDatabase

func CleanDatabase(group Group) error

CleanDatabase deletes the given group and associated systems, encryption keys, and secrets.

func ClientTokenCreated

func ClientTokenCreated(data Event)

ClientTokenCreated should be called every time a system client token is generated

func ConvertJWKToPEM

func ConvertJWKToPEM(jwks string) (string, error)

ConvertJWKToPEM extracts the (hopefully single) public key contained in a jwks. Modified from source at: https://play.golang.org/p/mLpOxS-5Fy

func ConvertPublicKeyToPEMString

func ConvertPublicKeyToPEMString(pk *rsa.PublicKey) (string, error)

func CreateTestXData

func CreateTestXData(t *testing.T, db *gorm.DB) (creds Credentials, group Group)

func CreateTestXDataV2

func CreateTestXDataV2(t *testing.T, db *gorm.DB) (creds Credentials, group Group)

func DeleteGroup

func DeleteGroup(ctx context.Context, id string) error

func ExpireAdminCreds

func ExpireAdminCreds()

func GeneratePublicKey

func GeneratePublicKey(bits int) (string, string, *rsa.PrivateKey, error)

func GenerateSecret

func GenerateSecret() (string, error)

func GenerateTestKeys

func GenerateTestKeys(bitSize int) (*rsa.PrivateKey, rsa.PublicKey, error)

func GetAllIPs

func GetAllIPs() ([]string, error)

func GetAuthorizedGroupsForOktaID

func GetAuthorizedGroupsForOktaID(ctx context.Context, oktaID string) ([]string, error)

GetAuthorizedGroupsForOktaID returns a slice of GroupID's representing all groups this Okta user has rights to manage TODO: this is the slowest and most memory intensive way possible to implement this. Refactor!

func GetLogger

func GetLogger(logger logrus.FieldLogger) *logrus.Logger

GetLogger returns the underlying implementation of the field logger

func OperationCalled

func OperationCalled(data Event)

OperationCalled will log the caller of an operation. The caller should use the same randomly generated TrackingID as used in the operation for OperationStarted, OperationSucceeded, etc.

func OperationFailed

func OperationFailed(data Event)

OperationFailed should be called after an event's failure, and should always be preceded by a call to OperationStarted

func OperationStarted

func OperationStarted(data Event)

OperationStarted should be called at the beginning of all logged events

func OperationSucceeded

func OperationSucceeded(data Event)

OperationSucceeded should be called after an event's success, and should always be preceded by a call to OperationStarted

func RandomBase64

func RandomBase64(n int) string

func RandomHexID

func RandomHexID() string

func RandomIPv4

func RandomIPv4() string

func RandomIPv6

func RandomIPv6() string

func ReadPEMFile

func ReadPEMFile(pemPath string) ([]byte, error)

ReadPEMFile reads the contents of the file found at pemPath, returning those contents as a string or an empty string with a non-nil error

func ReadPrivateKey

func ReadPrivateKey(privateKey []byte) (*rsa.PrivateKey, error)

ReadPrivateKey reads a PEM-formatted private key and returns a pointer to an rsa.PublicKey type and an error. The key must have a length of at least 2048 bits, and it must be an rsa key. It must also be the first and only key in the file.

func ReadPublicKey

func ReadPublicKey(publicKey string) (*rsa.PublicKey, error)

ReadPublicKey reads a string containing a PEM-formatted public key and returns a pointer to an rsa.PublicKey type or an error. The key must have a length of at least 2048 bits, and it must be an rsa key.

func ResetAdminCreds

func ResetAdminCreds() (encSecret string, err error)

func RevokeActiveCreds

func RevokeActiveCreds(groupID string) error

RevokeActiveCreds revokes all credentials for the specified GroupID

func SecretCreated

func SecretCreated(data Event)

SecretCreated should be called every time a system's secret is generated

func SecureHashTime

func SecureHashTime(data Event)

SecureHashTime should be called with the time taken to create a hash, logs of which can be used to approximate the security provided by the hash

func ServiceHalted

func ServiceHalted(data Event)

ServiceHalted should be called to log an unexpected stop to the service

func ServiceStarted

func ServiceStarted(data Event)

ServiceStarted should be called to log the starting of the service

func TokenBlacklisted

func TokenBlacklisted(data Event)

TokenBlacklisted records that a token with a specific key is invalidated

func TokenMintingFailure

func TokenMintingFailure(data Event)

TokenMintingFailure is emitted when a token can't be created. Usually, this is due to a issue with the signing key.

func ValidAddress

func ValidAddress(address string) bool

func VerifySignature

func VerifySignature(pubKey *rsa.PublicKey, signatureStr string) error

func XDataFor

func XDataFor(ctx context.Context, system System) (string, error)

DataForSystem returns the group extra data associated with this system

Types

type APILoggerEntry

type APILoggerEntry struct {
	Logger logrus.FieldLogger
}

type AuthRegData

type AuthRegData struct {
	GroupID         string
	AllowedGroupIDs []string
	OktaID          string
}

type BadRequestResponse

type BadRequestResponse struct {
	// Invalid request
	// in: body
	Body ErrorResponse
}

The request is invalid or malformed swagger:response badRequestResponse

type BlacklistEntry

type BlacklistEntry struct {
	gorm.Model
	Key             string `gorm:"not null" json:"key"`
	EntryDate       int64  `gorm:"not null" json:"entry_date"`
	CacheExpiration int64  `gorm:"not null" json:"cache_expiration"`
}

func CreateBlacklistEntry

func CreateBlacklistEntry(ctx context.Context, key string, entryDate time.Time, cacheExpiration time.Time) (entry BlacklistEntry, err error)

func GetUnexpiredBlacklistEntries

func GetUnexpiredBlacklistEntries(ctx context.Context) (entries []BlacklistEntry, err error)

type Caveats

type Caveats map[string]string

type ClientToken

type ClientToken struct {
	gorm.Model
	Label     string    `json:"label"`
	Uuid      string    `json:"uuid"`
	System    System    `gorm:"foreignkey:SystemID;association_foreignkey:ID"`
	SystemID  uint      `json:"system_id"`
	ExpiresAt time.Time `json:"expires_at"`
}

type ClientTokenOutput

type ClientTokenOutput struct {
	ID           string    `json:"id"`
	CreationDate time.Time `json:"creation_date"`
	Label        string    `json:"label"`
	UUID         string    `json:"uuid"`
	ExpiresAt    time.Time `json:"expires_at"`
}

func OutputCT

func OutputCT(cts ...ClientToken) []ClientTokenOutput

type ClientTokenResponse

type ClientTokenResponse struct {
	ClientTokenOutput
	Token string
}

type Credentials

type Credentials struct {
	ClientID     string    `json:"client_id,omitempty"`
	ClientSecret string    `json:"client_secret,omitempty"`
	ClientToken  string    `json:"client_token,omitempty"`
	SystemID     string    `json:"system_id"`
	ClientName   string    `json:"client_name"`
	IPs          []string  `json:"ips,omitempty"`
	ExpiresAt    time.Time `json:"expires_at"`
	XData        string    `json:"xdata,omitempty"`
	PublicKeyID  string    `json:"public_key_id"`
}

func RegisterSystem

func RegisterSystem(ctx context.Context, clientName string, groupID string, scope string, publicKeyPEM string, ips []string, trackingID string) (Credentials, error)

RegisterSystem will save a new system and public key after verifying provided details for validity. It returns a ssas.Credentials struct including the generated clientID and secret.

func RegisterV2System

func RegisterV2System(ctx context.Context, input SystemInput) (Credentials, error)

type EncryptionKey

type EncryptionKey struct {
	gorm.Model
	Body     string `json:"body"`
	System   System `gorm:"foreignkey:SystemID;association_foreignkey:ID"`
	SystemID uint   `json:"system_id"`
	UUID     string `json:"uuid,omitempty"`
}

type ErrorResponse

type ErrorResponse struct {
	// Error type
	// Required: true
	Error string `json:"error"`
	// More information about the error
	ErrorDescription string `json:"error_description"`
}

type Event

type Event struct {
	UserID     string
	ClientID   string
	Elapsed    time.Duration
	Help       string
	Op         string
	TokenID    string
	TrackingID string
}

Event contains the superset of fields that may be included in Logger statements

type Group

type Group struct {
	gorm.Model
	GroupID string    `gorm:"unique;not null" json:"group_id"`
	XData   string    `gorm:"type:text" json:"xdata"`
	Data    GroupData `gorm:"type:jsonb" json:"data"`
	Systems []System  `gorm:"foreignkey:GID"`
}

func CreateGroup

func CreateGroup(ctx context.Context, gd GroupData, trackingID string) (Group, error)

func GetGroupByGroupID

func GetGroupByGroupID(ctx context.Context, groupID string) (Group, error)

func GetGroupByID

func GetGroupByID(ctx context.Context, id string) (Group, error)

GetGroupByID returns the group associated with the provided ID

func UpdateGroup

func UpdateGroup(ctx context.Context, id string, gd GroupData) (Group, error)

type GroupData

type GroupData struct {
	GroupID   string     `json:"group_id"`
	Name      string     `json:"name"`
	XData     string     `json:"xdata"`
	Users     []string   `json:"users,omitempty"`
	Scopes    []string   `json:"scopes,omitempty"`
	Systems   []System   `gorm:"-" json:"systems,omitempty"`
	Resources []Resource `json:"resources,omitempty"`
}

func (*GroupData) Scan

func (gd *GroupData) Scan(value interface{}) error

Make the GroupData struct implement the sql.Scanner interface

func (GroupData) Value

func (gd GroupData) Value() (driver.Value, error)

Value implements the driver.Value interface for GroupData.

type GroupDataParam

type GroupDataParam struct {
	// Data necessary to create or update a group
	// in: body
	// required: true
	Body GroupInput `json:"group_input"`
}

swagger:parameters createGroup updateGroup

type GroupIDParam

type GroupIDParam struct {
	// ID of group
	// in: path
	// required: true
	GroupID string `json:"group_id"`
}

swagger:parameters updateGroup deleteGroup

type GroupInput

type GroupInput struct {
	// A user-specified unique identifier for the group
	// Example: 550ffb24-dd8a-439c-b700-dd664a66d5a7
	// Required: true
	GroupID string `json:"group_id"`
	// A human-readable name for the group
	// Example: My Test Group
	// Required: true
	Name string `json:"name"`
	// A packet of string data in JSON format that should be associated with this group's systems
	// Example: `{"cms_ids":["A9994"]}`
	// Required: true
	XData string `json:"xdata"`
	// Optional Okta user ID's that should be able to manage this group
	// Example: ["abcd123","wxyz456"]
	Users []string `json:"users,omitempty"`
	// Resources (e.g. which API's should be allowed) for systems in this group
	// Required: true
	Resources []Resource `json:"resources,omitempty"`
}

type GroupList

type GroupList struct {
	Count      int            `json:"count"`
	ReportedAt time.Time      `json:"reported_at"`
	Groups     []GroupSummary `json:"groups"`
}

func ListGroups

func ListGroups(ctx context.Context, trackingID string) (list GroupList, err error)

type GroupResponse

type GroupResponse struct {
	// Group details
	// in: body
	Body struct {
		// The group's ID
		// Required: true
		ID int `json:"id"`
		// Creation timestamp for the group
		// Required: false
		CreatedAt time.Time `json:"created_at"`
		// Last update timestamp for the group
		// Required: false
		UpdatedAt time.Time `json:"updated_at"`
		// The date at which the group was deleted.  This is unlikely to be present in API output.
		// Required: false
		DeletedAt time.Time `json:"deleted_at"`
		// The user-provided identifier for the group
		// Required: true
		GroupID string `json:"group_id"`
		// The user-provided data for the group, which should be associated with all systems in this group.
		// Required: true
		XData string `json:"xdata"`
		// A parsed version of the user-provided data
		// Required: true
		Data GroupSummary `json:"data"`
	}
}

The group was successfully created/altered swagger:response groupResponse

type GroupSummary

type GroupSummary struct {
	ID        uint            `json:"id"`
	GroupID   string          `json:"group_id"`
	XData     string          `json:"xdata"`
	CreatedAt time.Time       `json:"created_at"`
	Systems   []SystemSummary `json:"systems" gorm:"foreignkey:GID;association_foreignkey:ID"`
}

func (GroupSummary) TableName

func (GroupSummary) TableName() string

type GroupsResponse

type GroupsResponse struct {
	// List of groups
	// in: body
	Body struct {
		// The number of registered groups
		// Required: true
		Count int `json:"count"`
		// The time the request is received
		// Required: true
		ReportedAt time.Time `json:"reported_at"`
		// The list of groups currently registered
		// Required: true
		Groups []GroupSummary `json:"groups"`
	}
}

List of all registered groups swagger:response groupsResponse

type Hash

type Hash string

Hash is a cryptographically hashed string

func NewHash

func NewHash(source string) (Hash, error)

NewHash creates a Hash value from a source string The HashValue consists of the salt and hash separated by a colon ( : ) If the source of randomness fails it returns an error.

func (Hash) IsHashOf

func (h Hash) IsHashOf(source string) bool

IsHashOf accepts an unhashed string, which it first hashes and then compares to itself

func (Hash) String

func (h Hash) String() string

type IP

type IP struct {
	gorm.Model
	Address  string
	SystemID uint
}

type IPOutput

type IPOutput struct {
	ID           string    `json:"id"`
	CreationDate time.Time `json:"creation_date"`
	IP           string    `json:"ip"`
}

func OutputIP

func OutputIP(ips ...IP) []IPOutput

type InvalidCredentialsResponse

type InvalidCredentialsResponse struct {
	// Invalid credentials
	// in: body
	Body ErrorResponse
}

Required credentials are missing or invalid swagger:response invalidCredentials

type IpID

type IpID struct {
	// ID of IP address
	// in: path
	// required: true
	IpID string `json:"ip_id"`
}

swagger:parameters deleteSystemIP

type NotFoundResponse

type NotFoundResponse struct {
	// Resource not found
	// in: body
	Body ErrorResponse
}

The specified resource is not found swagger:response notFoundResponse

type OkResponse

type OkResponse struct{}

The requested operation was successful. There is no body returned. swagger:response okResponse

type PublicKeyInput

type PublicKeyInput struct {
	PublicKey string `json:"public_key"`
	Signature string `json:"signature"`
}

type PublicKeyOutput

type PublicKeyOutput struct {
	ID           string    `json:"id"`
	CreationDate time.Time `json:"creation_date"`
	Key          string    `json:"key"`
}

func OutputPK

func OutputPK(eks ...EncryptionKey) []PublicKeyOutput

type PublicKeyResponse

type PublicKeyResponse struct {
	// Public key details
	// in: body
	Body struct {
		// This system's client ID
		// Required: true
		ClientID string `json:"client_id"`
		// The public key (if any) registered for this system
		// Required: true
		PublicKey string `json:"public_key"`
	}
}

The specified system has a public key, which is returned swagger:response publicKeyResponse

type Resource

type Resource struct {
	ID   string `json:"id"`
	Name string `json:"name"`
	// Example: ["bcda-api"]
	Scopes []string `json:"scopes"`
}

type RootKey

type RootKey struct {
	gorm.Model
	UUID      string `gorm:"column:uuid"`
	Key       string
	ExpiresAt time.Time
	SystemID  uint
}

func NewRootKey

func NewRootKey(ctx context.Context, systemID uint, expiration time.Time) (*RootKey, error)

func (*RootKey) Generate

func (rk *RootKey) Generate(caveats []Caveats, location string) (string, error)

Generate - Generate a Macaroon from the Token configuration

func (*RootKey) IsExpired

func (rk *RootKey) IsExpired() bool

type Secret

type Secret struct {
	gorm.Model
	Hash     string `json:"hash"`
	System   System `gorm:"foreignkey:SystemID;association_foreignkey:ID"`
	SystemID uint   `json:"system_id"`
}

func (*Secret) IsExpired

func (secret *Secret) IsExpired() bool

IsExpired tests whether this secret has expired

type ServerErrorResponse

type ServerErrorResponse struct {
	// Internal server error
	// in: body
	Body ErrorResponse
}

An internal server error has occurred swagger:response serverError

type System

type System struct {
	gorm.Model
	GID            uint            `json:"g_id"`
	GroupID        string          `json:"group_id"`
	ClientID       string          `json:"client_id"`
	SoftwareID     string          `json:"software_id"`
	ClientName     string          `json:"client_name"`
	APIScope       string          `json:"api_scope"`
	EncryptionKeys []EncryptionKey `json:"encryption_keys,omitempty"`
	Secrets        []Secret        `json:"secrets,omitempty"`
	LastTokenAt    time.Time       `json:"last_token_at"`
	XData          string          `json:"xdata"`
}

func GetSystemByClientID

func GetSystemByClientID(ctx context.Context, clientID string) (System, error)

GetSystemByClientID returns the system associated with the provided clientID

func GetSystemByID

func GetSystemByID(ctx context.Context, id string) (System, error)

GetSystemByID returns the system associated with the provided ID

func GetSystemsByGroupIDString

func GetSystemsByGroupIDString(ctx context.Context, groupId string) ([]System, error)

GetSystemsByGroupIDString returns the systems associated with the provided groups.group_id

func UpdateSystem

func UpdateSystem(ctx context.Context, id string, v map[string]string) (System, error)

func (*System) AddAdditionalPublicKey

func (system *System) AddAdditionalPublicKey(publicKey io.Reader, signature string) (*EncryptionKey, error)

func (*System) DeleteClientToken

func (system *System) DeleteClientToken(ctx context.Context, tokenID string) error

func (*System) DeleteEncryptionKey

func (system *System) DeleteEncryptionKey(ctx context.Context, trackingID string, keyID string) error

DeleteEncryptionKey deletes the key associated with the current system.

func (*System) DeleteIP

func (system *System) DeleteIP(ctx context.Context, ipID string, trackingID string) error

DeleteIP soft-deletes an IP associated with a specific system

func (*System) FindEncryptionKey

func (system *System) FindEncryptionKey(ctx context.Context, trackingID string, keyId string) (EncryptionKey, error)

FindEncryptionKey retrieves the key by id associated with the current system.

func (*System) GetClientTokens

func (system *System) GetClientTokens(ctx context.Context, trackingID string) ([]ClientToken, error)

func (*System) GetEncryptionKey

func (system *System) GetEncryptionKey(ctx context.Context, trackingID string) (EncryptionKey, error)

GetEncryptionKey retrieves the key associated with the current system.

func (*System) GetEncryptionKeys

func (system *System) GetEncryptionKeys(ctx context.Context, trackingID string) ([]EncryptionKey, error)

GetEncryptionKeys retrieves the keys associated with the current system.

func (*System) GetIPs

func (system *System) GetIPs() ([]string, error)

func (*System) GetIPsData

func (system *System) GetIPsData(ctx context.Context) ([]IP, error)

func (*System) GetIps

func (system *System) GetIps(ctx context.Context, trackingID string) ([]IP, error)

func (*System) GetSecret

func (system *System) GetSecret(ctx context.Context) (Secret, error)

GetSecret will retrieve the hashed secret associated with the current system.

func (*System) RegisterIP

func (system *System) RegisterIP(ctx context.Context, address string, trackingID string) (IP, error)

func (*System) ResetSecret

func (system *System) ResetSecret(ctx context.Context, trackingID string) (Credentials, error)

ResetSecret creates a new secret for the current system.

func (*System) RevokeSecret

func (system *System) RevokeSecret(ctx context.Context, trackingID string) error

RevokeSecret revokes a system's secret

func (*System) SaveClientToken

func (system *System) SaveClientToken(ctx context.Context, label string, groupXData string, expiration time.Time) (*ClientToken, string, error)

SaveClientToken should be provided with a token label and token uuid, which will be saved to the client tokens table and associated with the current system.

func (*System) SavePublicKey

func (system *System) SavePublicKey(publicKey io.Reader, signature string) (*EncryptionKey, error)

SavePublicKey should be provided with a public key in PEM format, which will be saved to the encryption_keys table and associated with the current system.

func (*System) SavePublicKeyDB

func (system *System) SavePublicKeyDB(publicKey io.Reader, signature string, onlyOne bool, db *gorm.DB) (*EncryptionKey, error)

func (*System) SaveSecret

func (system *System) SaveSecret(ctx context.Context, hashedSecret string) error

SaveSecret should be provided with a secret hashed with ssas.NewHash(), which will be saved to the secrets table and associated with the current system.

func (*System) SaveTokenTime

func (system *System) SaveTokenTime(ctx context.Context)

SaveTokenTime puts the current time in systems.last_token_at

type SystemDataParam

type SystemDataParam struct {
	// Data necessary to create a system
	// in: body
	// required: true
	Body SystemInput `json:"system_input"`
}

swagger:parameters createSystem

type SystemIDParam

type SystemIDParam struct {
	// ID of system
	// in: path
	// required: true
	SystemID string `json:"system_id"`
}

swagger:parameters getPublicKey resetCredentials deleteCredentials deleteSystemIP

type SystemInput

type SystemInput struct {
	// A user-specified name for the system
	// Example: My Test System
	// Required: true
	ClientName string `json:"client_name"`
	// The group ID (user-specified unique string value) that the system should be attached to
	// Example: My Test Group
	// Required: true
	GroupID string `json:"group_id"`
	// The scope for this system
	// Example: bcda-api
	// Required: true
	Scope string `json:"scope"`
	// An optional RSA 2048-bit public key to register with this system
	PublicKey string `json:"public_key"`
	// An optional signature to verify the public key
	Signature string `json:"signature,omitempty"`
	// An optional list of public IP addresses (IPv4 or IPv6) from which this system can make requests
	IPs []string `json:"ips"`
	// A unique identifier for this request to assist in log correlation
	// Required: true
	TrackingID string `json:"tracking_id"`

	XData string `json:"xdata,omitempty"`
}

type SystemOutput

type SystemOutput struct {
	GID          string              `json:"g_id"`
	GroupID      string              `json:"group_id"`
	ClientID     string              `json:"client_id"`
	SoftwareID   string              `json:"software_id"`
	ClientName   string              `json:"client_name"`
	APIScope     string              `json:"api_scope"`
	XData        string              `json:"x_data"`
	LastTokenAt  string              `json:"last_token_at"`
	PublicKeys   []PublicKeyOutput   `json:"public_keys"`
	IPs          []IPOutput          `json:"ips"`
	ClientTokens []ClientTokenOutput `json:"client_tokens"`
}

type SystemResponse

type SystemResponse struct {
	// System details
	// in: body
	Body struct {
		// The client ID for this system
		// Required: true
		ClientID string `json:"client_id"`
		// The client secret for this system
		// Required: true
		ClientSecret string `json:"client_secret"`
		// This system's ID
		// Required: true
		SystemID string `json:"system_id"`
		// The user-specified name for the system
		// Required: true
		ClientName string `json:"client_name"`
		// The expiration date for these credentials
		// Required: true
		ExpiresAt time.Time `json:"expires_at"`
		// Optional IP addresses from which this system is allowed to connect
		// Required: false
		IPs []string `json:"ips,omitempty"`
	}
}

The successfully created/altered system is returned swagger:response systemResponse

type SystemSummary

type SystemSummary struct {
	ID         uint      `json:"id"`
	GID        uint      `json:"-"`
	ClientName string    `json:"client_name"`
	ClientID   string    `json:"client_id"`
	IPs        []string  `json:"ips,omitempty" gorm:"-"`
	UpdatedAt  time.Time `json:"updated_at"`
}

func (SystemSummary) TableName

func (SystemSummary) TableName() string

type TokenIDParam

type TokenIDParam struct {
	// A token's ID
	// in: path
	// required: true
	TokenID string `json:"token_id"`
}

swagger:parameters revokeToken

Directories

Path Synopsis
main
Package main System-to-System Authentication Service
Package main System-to-System Authentication Service
public
Package public (ssas/service/api/public) contains API functions, middleware, and a router designed to:
Package public (ssas/service/api/public) contains API functions, middleware, and a router designed to:

Jump to

Keyboard shortcuts

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