sams

package module
v0.0.0-...-67ba7d2 Latest Latest
Warning

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

Go to latest
Published: Dec 11, 2024 License: Apache-2.0 Imports: 25 Imported by: 0

README

Sourcegraph Accounts SDK for Go

Go Reference Go

This repository contains the Go SDK for integrating with Sourcegraph Accounts Management System (SAMS).

go get github.com/sourcegraph/sourcegraph-accounts-sdk-go

[!note] Please reach out to #discuss-core-services for questions and help, and when guided, submit all issues to the Core Services project on Linear.

Authentication

The following example demonstrates how to use the SDK to set up user authentication flow with SAMS for your service.

In particular,

  • The route /auth/login is where the user should be redirected to start a new authentication flow.
  • The route /auth/callback is where the user will be redirected back to the service after completing the authentication on the SAMS side.
package main

import (
	"log"
	"net/http"
	"os"

	samsauth "github.com/sourcegraph/sourcegraph-accounts-sdk-go/auth"
	"github.com/sourcegraph/sourcegraph-accounts-sdk-go/scopes"
)

type secretStore struct{
	// Authentication state is the unique identifier that is randomly-generated and
	// assigned to a particular authentication flow, they are used to prevent
	// authentication interception attacks and considered secrets, therefore it MUST
	// be stored in a backend component (e.g. Redis, database). The design of the
	// samsauth.StateStore interface explicitly disallowed storing state in the
	// cookie, as they can be tampered with when cookie values are stored
	// unencrypted.
	//
	// Authentication nonce is a unique identifier that is randomly-generated to
	// make sure the ID Token we get back from SAMS is intended for the same
	// authentication flow that we started. It is also a secret and MUST be stored
	// in a backend component.
}

func (s *secretStore) SetState(r *http.Request, state string) error {
	// TODO: Save state to session data.
	return nil
}

func (s *secretStore) GetState(r *http.Request) (string, error) {
	// TODO: Retrieve state from session data.
	return "", nil
}

func (s *secretStore) DeleteState(r *http.Request) {
	// TODO: Delete state from session data.
}

func (s *secretStore) SetNonce(r *http.Request, nonce string) error {
	// TODO: Save nonce to session data.
	return nil
}

func (s *secretStore) GetNonce(r *http.Request) (string, error) {
	// TODO: Retrieve nonce from session data.
	return "", nil
}

func (s *secretStore) DeleteNonce(r *http.Request) {
	// TODO: Delete nonce from session data.
}

func main() {
	samsauthHandler, err := samsauth.NewHandler(
		samsauth.Config{
			Issuer:         "https://accounts.sourcegraph.com",
			ClientID:       os.Getenv("SAMS_CLIENT_ID"),
			ClientSecret:   os.Getenv("SAMS_CLIENT_SECRET"),
			// RequestScopes needs to include all the scopes that the service needs to
			// access on behalf of the user. Scopes that are only used for Clients API are
			// not needed here.
			RequestScopes:  []scopes.Scope{scopes.OpenID, scopes.Email, scopes.Profile},
			RedirectURI:    os.Getenv("SAMS_REDIRECT_URI"),
			FailureHandler: samsauth.DefaultFailureHandler,
			StateStore:     &stateStore{},
		},
	)
	if err != nil {
		log.Fatal(err)
	}

	mux := http.NewServeMux()
	mux.Handle("/auth/login", samsauthHandler.LoginHandler())
	mux.Handle("/auth/callback", samsauthHandler.CallbackHandler(
		// The SAMS auth handler will handle the callback and complete the
		// authentication flow. And if successful, the `samsauth.UserInfo` will be
		// accessible from the request context.
		//
		// You can safely assume the `samsauth.UserInfo` will be present when this
		// user-supplied handler is being invoked. If any error is encountered, the
		// provided FailureHandler will be invoked instead.
		http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
			userInfo := samsauth.UserInfoFromContext(r.Context())
			// TODO: Save user info to somewhere.
		}),
	))

	// Continue setting up your server and use the mux.
}

Clients API v1

The SAMS Clients API is for SAMS clients to obtain information directly from SAMS. For example, authorizing a request based on the scopes attached to a token. Or looking up a user's profile information based on the SAMS external account ID.

package main

import (
	"context"
	"fmt"
	"log"
	"os"

	sams "github.com/sourcegraph/sourcegraph-accounts-sdk-go"
	"github.com/sourcegraph/sourcegraph-accounts-sdk-go/scopes"
)

func main() {
	connConfig := sams.NewConnConfigFromEnv(/* ... */)
	samsClient, err := sams.NewClientV1(sams.ClientV1Config{
		ConnConfig:  connConfig,
		TokenSource: sams.ClientCredentialsTokenSource(
			connConfig,
			os.Getenv("SAMS_CLIENT_ID"),
			os.Getenv("SAMS_CLIENT_SECRET"),
			[]scopes.Scope{
				scopes.OpenID,
				scopes.Profile,
				scopes.Email,
				"sams::user.roles::read",
				"sams::session::read",
			},
		),
	})
	if err != nil {
		log.Fatal(err)
	}
	
	user, err := samsClient.Users().GetUserByID(context.Background(), "user-id")
	if err != nil {
		log.Fatal(err)
	}
	fmt.Println(user)
}

Accounts API v1

The SAMS Accounts API is for user-oriented operations like inspecting your own account details. These APIs are much simpler in nature, as most integrations will make use of the Clients API. However, the Accounts API is required if the service is not governing access based on the SAMS token scope, but instead using its own authorization mechanism. e.g. governing access based on the SAMS external account ID.

package main

import (
	"context"
	"fmt"
	"log"
	"os"

	"golang.org/x/oauth2"
	sams "github.com/sourcegraph/sourcegraph-accounts-sdk-go"
)

func main() {
	// e.g. the SAMS token prefixed with "sams_at_".
	rawToken := os.Getenv("SAMS_USER_ACCESS_TOKEN")

	// If you have the SAMS user's Refresh token, using the oauth2.TokenSource abstraction
	// will take care of creating short-lived access tokens as needed. But if you only have
	// the access token, you will need to use a StaticTokenSource instead.
	token := oauth2.Token{
		AccessToken: rawToken,
	}
	tokenSource := oauth2.StaticTokenSource(t)

	client := sams.NewAccountsV1(sams.AccountsV1Config{
		ConnConfig:  sams.NewConnConfigFromEnv(/* ... */),
		TokenSource: tokenSource,
	})
	user, err := client.GetUser(ctx)
	if err != nil {
		log.Fatal(err)
	}
	fmt.Printf("User Details: %+v", user)
}

Notifications API v1

[!note] For integrating MSP services, please refer to the handbook page SAMS notifications distribution system for a step-by-step integration guidance.

The SAMS Notifications API is for distributing notifications to downstream systems for them to take appropriate actions. For example, notifying systems that a user has been deleted.

package main

import (
	"context"
	"fmt"
	"os"
	"time"

	"github.com/sourcegraph/log"
	sams "github.com/sourcegraph/sourcegraph-accounts-sdk-go"
	notificationsv1 "github.com/sourcegraph/sourcegraph-accounts-sdk-go/notifications/v1"
)

func main() {
	var logger log.Logger // TODO: Initialize your logger.

	handlers := notificationsv1.SubscriberHandlers{
		OnUserDeleted: func(ctx context.Context, data *notificationsv1.UserDeletedData) error {
			fmt.Printf("User %q (%s) has been deleted.\n", data.AccountID, data.Email)
			return nil
		},
	}

	subscriber, err := sams.NewNotificationsV1Subscriber(
		logger,
		// In MSP, you can use `sams.NewNotificationsV1SubscriberConfigFromEnv` to derive some configurations from the environment variables.
		notificationsv1.SubscriberOptions{
			ProjectID: os.Getenv("GOOGLE_CLOUD_PROJECT"),
			SubscriptionID: os.Getenv("SAMS_NOTIFICATION_SUBSCRIPTION"),
			ReceiveSettings: notificationsv1.DefaultReceiveSettings,
			Handlers: handlers,
		},
	)
	if err != nil {
		logger.Fatal("failed to create notification subscriber", log.Error(err))
		return
	}
	go subscriber.Start()

	// For demonstration purposes, we will run the subscriber for 1 minute.
	time.Sleep(time.Minute)
	subscriber.Stop() // Stop receiving notifications.
}

Development

Buf and Connect are used for gRPC and Protocol Buffers code generation.

go install github.com/bufbuild/buf/cmd/buf@latest
go install google.golang.org/protobuf/cmd/protoc-gen-go@latest
go install connectrpc.com/connect/cmd/protoc-gen-connect-go@latest

After making any changes to the .proto files, in the direction that contains the buf.gen.yaml file, run:

buf generate

Documentation

Index

Constants

View Source
const DefaultExternalURL = "https://accounts.sourcegraph.com"

Variables

View Source
var (
	ErrNotFound       = errors.New("not found")
	ErrRecordMismatch = errors.New("record mismatch")
	// ErrAborted is returned due to a concurrency conflict.
	// e.g. Two clients trying to perform an operation at the same time for the same resource.
	// It is safe to retry the request at a later time.
	ErrAborted = errors.New("aborted")
)

Functions

func ClientCredentialsTokenSource

func ClientCredentialsTokenSource(conn ConnConfig, clientID, clientSecret string, requestScopes []scopes.Scope) oauth2.TokenSource

ClientCredentialsTokenSource returns a TokenSource that generates an access token using the client credentials flow. Internally, the token returned is reused. So that new tokens are only created when needed. (Provided this `Client` is long-lived.)

The `requestScopes` is a list of scopes to be requested for this token source. Scopes should be defined using the available scopes package. All requested scopes must be allowed by the registered client - see: https://sourcegraph.notion.site/6cc4a1bd9cb247eea9674dbf9d5ce8c3

func NewAccountsV1

func NewAccountsV1(config AccountsV1Config) (*accountsv1.Client, error)

NewAccountsV1 returns a new SAMS client for interacting with Accounts API V1.

func NewNotificationsV1Subscriber

func NewNotificationsV1Subscriber(logger log.Logger, opts notificationsv1.SubscriberOptions) (background.Routine, error)

NewNotificationsV1Subscriber returns a new background routine for receiving SAMS notifications with v1 protocol.

Types

type AccountsV1Config

type AccountsV1Config struct {
	ConnConfig
	// TokenSource is the OAuth2 token source to use for authentication. It MUST be
	// based on a per-user token that is on behalf of a SAMS user.
	//
	// If you have the SAMS user's refresh token, using the oauth2.TokenSource
	// abstraction will take care of creating short-lived access tokens and refresh
	// as needed. But if you only have the access token, you will need to use a
	// StaticTokenSource instead.
	TokenSource oauth2.TokenSource
}

func (AccountsV1Config) Validate

func (c AccountsV1Config) Validate() error

type ClientV1

type ClientV1 struct {
	// contains filtered or unexported fields
}

ClientV1 provides helpers to talk to a SAMS instance via Clients API v1.

func NewClientV1

func NewClientV1(config ClientV1Config) (*ClientV1, error)

NewClientV1 returns a new SAMS client for interacting with Clients API v1 using the given client credentials, and the scopes are used to as requested scopes for access tokens that are issued to this client.

func (*ClientV1) Roles

func (c *ClientV1) Roles() *RolesServiceV1

func (*ClientV1) Sessions

func (c *ClientV1) Sessions() *SessionsServiceV1

Sessions returns a client handler to interact with the SessionsServiceV1 API.

func (*ClientV1) Tokens

func (c *ClientV1) Tokens() *TokensServiceV1

Tokens returns a client handler to interact with the TokensServiceV1 API.

func (*ClientV1) Users

func (c *ClientV1) Users() *UsersServiceV1

Users returns a client handler to interact with the UsersServiceV1 API.

type ClientV1Config

type ClientV1Config struct {
	ConnConfig
	// TokenSource is the OAuth2 token source to use for authentication. It MUST be
	// based on a per-client token that is on behalf of a SAMS client (i.e. Clients
	// Credentials).
	//
	// The oauth2.TokenSource abstraction will take care of creating short-lived
	// access tokens as needed.
	TokenSource oauth2.TokenSource
	// SessionsCacheSize is the number of sessions to cache in memory.
	//
	// The default of 0 (or less) disables caching.
	SessionsCacheSize int
	// IntrospectTokenCacheSize is the number of token introspection results to
	// cache in memory.
	//
	// The default of 0 (or less) disables caching.
	IntrospectTokenCacheSize int
}

func (ClientV1Config) Validate

func (c ClientV1Config) Validate() error

type ConnConfig

type ConnConfig struct {
	// ExternalURL is the configured default external ExternalURL of the relevant Sourcegraph
	// Accounts instance.
	ExternalURL string
	// APIURL is the URL to use for Sourcegraph Accounts API interactions. This
	// can be set to some internal URLs for private networking. If this is nil,
	// the client will fall back to ExternalURL instead.
	APIURL *string
}

ConnConfig is the basic configuration for connecting to a Sourcegraph Accounts instance. Callers SHOULD use sams.NewConnConfigFromEnv(...) to construct this where possible to ensure connection configuration is unified across SAMS clients for ease of operation by Core Services team.

func NewConnConfigFromEnv

func NewConnConfigFromEnv(env envGetter) ConnConfig

NewConnConfigFromEnv initializes configuration for connecting to Sourcegraph Accounts using default standards for loading environment variables. This allows the Core Services team to more easily configure access.

func (ConnConfig) Validate

func (c ConnConfig) Validate() error

type IntrospectTokenResponse

type IntrospectTokenResponse struct {
	// Active indicates whether the token is currently active. The value is "true"
	// if the token has been issued by the SAMS instance, has not been revoked, and
	// has not expired.
	Active bool
	// Scopes is the list of scopes granted by the token.
	Scopes scopes.Scopes
	// ClientID is the identifier of the SAMS client that the token was issued to.
	ClientID string
	// ExpiresAt indicates when the token expires.
	ExpiresAt time.Time
}

type NotificationsV1SubscriberConfig

type NotificationsV1SubscriberConfig struct {
	// ProjectID is the GCP project ID that the Pub/Sub subscription belongs to. It
	// is almost always the same GCP project that the Cloud Run service is deployed
	// to.
	ProjectID string
	// SubscriptionID is the GCP Pub/Sub subscription ID to receive SAMS
	// notifications from.
	SubscriptionID string
}

NotificationsV1SubscriberConfig holds configuration for the SAMS notifications that are derived from the environment variables, HOWEVER, this is not a complete configuration to create a notification subscriber.

func NewNotificationsV1SubscriberConfigFromEnv

func NewNotificationsV1SubscriberConfigFromEnv(env envGetter) NotificationsV1SubscriberConfig

NewNotificationsV1SubscriberConfigFromEnv initializes configuration based on environment variables.

type RegisterResourcesMetadata

type RegisterResourcesMetadata struct {
	ResourceType roles.ResourceType
}

RegisterResourcesMetadata is the metadata for a set of resources to be registered.

type RolesServiceV1

type RolesServiceV1 struct {
	// contains filtered or unexported fields
}

RolesServiceV1 provides client methods to interact with the RolesService API v1.

func (*RolesServiceV1) RegisterRoleResources

func (s *RolesServiceV1) RegisterRoleResources(ctx context.Context, metadata RegisterResourcesMetadata, resourcesIterator func() ([]*clientsv1.RoleResource, error)) (uint64, error)

RegisterRoleResources registers the resources for a given resource type. `resourcesIterator` is a function that returns a page of resources to register. The function is invoked repeatedly until it produces an empty slice or an error. If another replica is already registering resources for the same resource type the function will return 0 with ErrAborted. ErrAborted means the request is safe to retry at a later time.

Required scope: sams::roles.resources::write

type SessionsServiceV1

type SessionsServiceV1 struct {
	// contains filtered or unexported fields
}

SessionsServiceV1 provides client methods to interact with the SessionsService API v1.

func (*SessionsServiceV1) GetSessionByID

func (s *SessionsServiceV1) GetSessionByID(ctx context.Context, id string) (*clientsv1.Session, error)

GetSessionByID returns the SAMS session with the given ID. It returns ErrNotFound if no such session exists. The session's `User` field is populated if the session is authenticated by a user.

Required scope: sams::session::read

func (*SessionsServiceV1) SignOutSession

func (s *SessionsServiceV1) SignOutSession(ctx context.Context, sessionID, userID string) error

SignOutSession revokes the authenticated state of the session with the given ID for the given user. It does not return error if the session does not exist or is not authenticated. It returns ErrRecordMismatch if the session is authenticated by a different user than the given user.

Required scope: sams::session::write

type TokensServiceV1

type TokensServiceV1 struct {
	// contains filtered or unexported fields
}

TokensServiceV1 provides client methods to interact with the TokensService API v1.

func (*TokensServiceV1) IntrospectToken

func (s *TokensServiceV1) IntrospectToken(ctx context.Context, token string) (*IntrospectTokenResponse, error)

IntrospectToken takes a SAMS access token and returns relevant metadata.

🚨SECURITY: SAMS will return a successful result if the token is valid, but is no longer active. It is critical that the caller not honor tokens where `.Active == false`.

type UsersServiceV1

type UsersServiceV1 struct {
	// contains filtered or unexported fields
}

UsersServiceV1 provides client methods to interact with the UsersService API v1.

func (*UsersServiceV1) GetUserByEmail

func (s *UsersServiceV1) GetUserByEmail(ctx context.Context, email string) (*clientsv1.User, error)

GetUserByEmail returns the SAMS user with the given verified email. It returns ErrNotFound if no such user exists.

Required scope: profile

func (*UsersServiceV1) GetUserByID

func (s *UsersServiceV1) GetUserByID(ctx context.Context, id string) (*clientsv1.User, error)

GetUserByID returns the SAMS user with the given ID. It returns ErrNotFound if no such user exists.

Required scope: profile

func (*UsersServiceV1) GetUserMetadata

func (s *UsersServiceV1) GetUserMetadata(ctx context.Context, userID string, namespaces []string) ([]*clientsv1.UserServiceMetadata, error)

GetUserMetadata returns the metadata associated with the given user ID and metadata namespaces.

Required scopes: sams::user.metadata.${NAMESPACE}::read for each of the requested namespaces.

func (*UsersServiceV1) GetUserRolesByID

func (s *UsersServiceV1) GetUserRolesByID(ctx context.Context, userID, service string) ([]*clientsv1.Role, error)

GetUserRolesByID returns all roles that have been assigned to the SAMS user with the given ID and scoped by the service.

Required scopes: sams::user.roles::read

func (*UsersServiceV1) GetUsersByIDs

func (s *UsersServiceV1) GetUsersByIDs(ctx context.Context, ids []string) ([]*clientsv1.User, error)

GetUsersByIDs returns the list of SAMS users matching the provided IDs.

NOTE: It silently ignores any invalid user IDs, i.e. the length of the return slice may be less than the length of the input slice.

Required scopes: profile

func (*UsersServiceV1) UpdateUserMetadata

func (s *UsersServiceV1) UpdateUserMetadata(ctx context.Context, userID, namespace string, metadata map[string]any) (*clientsv1.UserServiceMetadata, error)

UpdateUserMetadata updates the metadata associated with the given user ID and metadata namespace.

Required scopes: sams::user.metadata.${NAMESPACE}::read for the namespace being updated.

Directories

Path Synopsis
accounts
v1
clients
v1
notifications
v1

Jump to

Keyboard shortcuts

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