Documentation
¶
Overview ¶
Package console is the Slack module's half of the console: what an operator does to Slack through the console (connecting a workspace, creating and installing Apps, defining channels and Slack Connect channels) runs here, on the module's own records and credentials, as the caller the console authorized (docs/decisions/0071, v1.76 D-v176-1 b).
The console keeps what is the browser's: it signs the person in, takes the coarse role, signs the state of an install and pins the browser to it, and renders the page a callback lands on. It asks this package for the rest through modcall, as the caller class `console` and no other; Register puts the methods on the module's server. Authorization against the module's own records (who owns a workspace) runs here, on the caller snapshot, through internal/consoleauthz.
A handler returns a Connect error for what the person is told, as the console always did; Register turns it into what crosses the wire: a refusal the transport carries (forbidden, unauthenticated), a consolecall.Problem for every other answer to the question asked, and nothing but a code for a failure of the module's own.
Index ¶
- Constants
- func AppSettingsURL(appID string) string
- func CodeOf(p *consolecall.Problem) connect.Code
- func NewServer(c *Service) *modcall.Server
- func Register(s *modcall.Server, c *Service)
- func RegisterLazy(s *modcall.Server, log *slog.Logger, ...)
- func ValidAppID(id string) bool
- type APIOption
- type Authorizer
- type Book
- type Catalogue
- type CatalogueApps
- type ChannelRecords
- type CompleteRequest
- type Completion
- type Deps
- type Refusal
- type Service
- func (c *Service) BeginSlackWorkspaceConnect(ctx context.Context, caller consolecall.Caller, ...) (*directoryrosterv1.BeginSlackWorkspaceConnectResponse, error)
- func (c *Service) Book(ctx context.Context) (Book, error)
- func (c *Service) ChangeSlackWorkspaceOwner(ctx context.Context, caller consolecall.Caller, ...) (*directoryrosterv1.ChangeSlackWorkspaceOwnerResponse, error)
- func (c *Service) CompleteCatalogue(ctx context.Context, caller consolecall.Caller, req CompleteRequest) (Completion, error)
- func (c *Service) CompleteWorkspace(ctx context.Context, caller consolecall.Caller, req CompleteRequest) (Completion, error)
- func (c *Service) ConfirmSlackRemovals(ctx context.Context, caller consolecall.Caller, ...) (*directoryrosterv1.ConfirmSlackRemovalsResponse, error)
- func (c *Service) CreateSlackApp(ctx context.Context, caller consolecall.Caller, ...) (*directoryrosterv1.CreateSlackAppResponse, error)
- func (c *Service) CreateSlackChannel(ctx context.Context, caller consolecall.Caller, ...) (*directoryrosterv1.CreateSlackChannelResponse, error)
- func (c *Service) CreateSlackSharedChannel(ctx context.Context, caller consolecall.Caller, ...) (*directoryrosterv1.CreateSlackSharedChannelResponse, error)
- func (c *Service) DeleteSlackChannel(ctx context.Context, caller consolecall.Caller, ...) (*directoryrosterv1.DeleteSlackChannelResponse, error)
- func (c *Service) DeleteSlackSharedChannel(ctx context.Context, caller consolecall.Caller, ...) (*directoryrosterv1.DeleteSlackSharedChannelResponse, error)
- func (c *Service) DisconnectSlackWorkspace(ctx context.Context, caller consolecall.Caller, ...) (*directoryrosterv1.DisconnectSlackWorkspaceResponse, error)
- func (c *Service) GetSlackStatus(ctx context.Context, caller consolecall.Caller, ...) (*directoryrosterv1.GetSlackStatusResponse, error)
- func (c *Service) InstallSlackApp(ctx context.Context, caller consolecall.Caller, ...) (*directoryrosterv1.InstallSlackAppResponse, error)
- func (c *Service) ListSlackApps(ctx context.Context, caller consolecall.Caller, ...) (*directoryrosterv1.ListSlackAppsResponse, error)
- func (c *Service) ListSlackChannels(ctx context.Context, caller consolecall.Caller, ...) (*directoryrosterv1.ListSlackChannelsResponse, error)
- func (c *Service) ListSlackSharedChannels(ctx context.Context, caller consolecall.Caller, ...) (*directoryrosterv1.ListSlackSharedChannelsResponse, error)
- func (c *Service) RequestSlackPass(ctx context.Context, caller consolecall.Caller, ...) (*directoryrosterv1.RequestSlackPassResponse, error)
- func (c *Service) RequireAnySlack(ctx context.Context, caller consolecall.Caller, want access.Role) (access.Identity, Book, error)
- func (c *Service) RequireSlack(ctx context.Context, caller consolecall.Caller, want access.Role, ...) (access.Identity, error)
- func (c *Service) UpdateSlackChannel(ctx context.Context, caller consolecall.Caller, ...) (*directoryrosterv1.UpdateSlackChannelResponse, error)
- func (c *Service) UpdateSlackSharedChannel(ctx context.Context, caller consolecall.Caller, ...) (*directoryrosterv1.UpdateSlackSharedChannelResponse, error)
- func (c *Service) WorkspaceStore() (Workspaces, error)
- type SharedRecords
- type StatusReports
- type Workspaces
Constants ¶
const ( // CatalogueCallbackPath is where Slack sends the browser back to after an // owner installs a catalogue App: a route of the console. CatalogueCallbackPath = "/connect/slack/catalogue/callback" // CatalogueBind prefixes the App's id in a catalogue flow's signed state, // so no other flow's state can finish it, and the other way round. CatalogueBind = "slack-catalogue:" )
The states a workspace's connection is shown in, and a catalogue App's.
const ( MethodBeginWorkspaceConnect = "console.begin_workspace_connect" MethodCompleteWorkspace = "console.complete_workspace" MethodDisconnectWorkspace = "console.disconnect_workspace" MethodChangeOwner = "console.change_owner" MethodRequestPass = "console.request_pass" MethodGetStatus = "console.get_status" MethodConfirmRemovals = "console.confirm_removals" MethodListApps = "console.list_apps" MethodCreateApp = "console.create_app" MethodInstallApp = "console.install_app" MethodCompleteCatalogue = "console.complete_catalogue" MethodListChannels = "console.list_channels" MethodCreateChannel = "console.create_channel" MethodUpdateChannel = "console.update_channel" MethodDeleteChannel = "console.delete_channel" )
The console methods of the Slack module, as `slack.<method>`. Each answers the caller class `console` alone (consolecall.Handle).
const Module = "slack"
Module is the module name the methods are registered under.
const WorkspaceBind = "slack-workspace:"
WorkspaceBind prefixes the workspace key in a workspace connect's signed state, so no other flow's state can finish it, and the other way round.
const WorkspaceCallbackPath = "/connect/slack/workspace/callback"
WorkspaceCallbackPath is where Slack sends the browser back to after an owner installs the roster's App into a workspace: a route of the console.
Variables ¶
This section is empty.
Functions ¶
func AppSettingsURL ¶
AppSettingsURL is where an App is managed and deleted.
func CodeOf ¶
func CodeOf(p *consolecall.Problem) connect.Code
CodeOf is the Connect code a Problem names, a failed precondition when it names none.
func NewServer ¶
NewServer is the module's server with the console methods on it: what a deployment that has not split the module out calls in its own process.
func RegisterLazy ¶
func RegisterLazy(s *modcall.Server, log *slog.Logger, open func(context.Context) (*Service, error))
RegisterLazy puts the console methods on the server over a service that open assembles on the first call of a container and returns again for the rest: a scheduled pass needs none of it. A service that cannot be opened is a call the transport answers unavailable, with no detail, and the next call tries again.
func ValidAppID ¶
ValidAppID reports whether id is a catalogue App's id.
Types ¶
type Authorizer ¶
Authorizer is the policy in force, which the module reads.
type Book ¶
type Book map[string]connection.Record
Book is every connected Slack workspace's record by key. What the policy leaves out of a workspace — which team it is, which directory owns it — is read here, from what connecting it recorded.
func (Book) May ¶
May reports whether an identity holds a role over one Slack workspace: by the recorded owner once it is connected. A workspace nobody has connected has no owner to be a viewer or an operator of; the one thing to do with it is connect it, so whoever could (consoleauthz.ResolveOwner) is let in.
func (Book) MayAct ¶
MayAct is whether the caller passes Service.RequireSlack for the workspace: the operator of its recorded owner, or installation-wide, which is all anyone but the installation-wide operator can be of a workspace that has no owner.
type Catalogue ¶
type Catalogue = slackcatalogue.Catalogue
Catalogue is every Slack App a deployment declares.
type CatalogueApps ¶
type CatalogueApps interface {
Put(ctx context.Context, record catalogueapp.Record, credentials catalogueapp.Credentials) error
List(ctx context.Context) ([]catalogueapp.Record, error)
// Get is one App's record and credentials, installed or not.
Get(ctx context.Context, id string) (catalogueapp.Record, catalogueapp.Credentials, bool, error)
}
CatalogueApps is where catalogue Slack Apps are kept: every App's record, client credentials and bot token, by its catalogue id.
type ChannelRecords ¶
type ChannelRecords interface {
List(ctx context.Context) ([]connection.ChannelRecord, error)
// Apply reads one record (nil when there is none), asks decide what it
// becomes (nil deletes it) and writes that under the object's version.
// decide is given every record as read, for a check across records.
Apply(ctx context.Context, workspace, name string,
decide func(current *reconcile.ConsoleChannel, all []connection.ChannelRecord) (*reconcile.ConsoleChannel, error)) error
}
ChannelRecords is where console channels' records are kept: the records the Slack controller reads.
type CompleteRequest ¶
type CompleteRequest struct {
// Bind is what the verified state binds: the workspace's or the App's key,
// with the flow's prefix.
Bind string `json:"bind"`
// Code is Slack's one-time code for the bot token.
Code string `json:"code,omitempty"`
// Denied is the `error` Slack sent instead of a code, when the install was
// not approved.
Denied string `json:"denied,omitempty"`
// Recheck says the caller is the person signed in now, whose role is asked
// again by the rule of the call that began the flow. A flow finished by
// someone who is not signed in here rests on the state alone.
Recheck bool `json:"recheck,omitempty"`
}
CompleteRequest is what the console forwards of Slack's redirect, once it has checked the signed state and the cookie the flow was pinned with. The code is single use: a call that spends it is never repeated, so a lost answer is a lost install, to be started again.
type Completion ¶
type Completion struct {
Refused *Refusal `json:"refused,omitempty"`
}
Completion is what became of a redirect: nothing to say when it finished, or the refusal the console shows on the page the browser lands on.
type Deps ¶
type Deps struct {
// Log is where what is not an audit record goes; nil is the default logger.
Log *slog.Logger
// Authorizer is the policy in force.
Authorizer Authorizer
// SlackCatalogue is every Slack App the deployment declares. Nil declares
// none.
SlackCatalogue *slackcatalogue.Catalogue
// SlackCatalogueApps is where catalogue Slack Apps are kept.
SlackCatalogueApps CatalogueApps
SlackShared SharedRecords
// SlackChannels is where console channels' records are kept.
SlackChannels ChannelRecords
// SlackStatus is what the Slack controller last reported.
SlackStatus StatusReports
// SlackWorkspaces is where connected workspaces and operators'
// confirmations are kept.
SlackWorkspaces Workspaces
// SlackAPI configures the calls to Slack: a test points them at a fake.
SlackAPI []slackapp.Option
// Audit records what an identity did. Nil records nothing.
Audit audit.Recorder
// Trigger is told which workspace a write concerns, so the controller's pass
// over it runs soon. A notification is a hint: failing to send one is
// logged. Nil notifies nobody.
Trigger port.Trigger
}
Deps is everything the module's console methods need. A store that is nil is a deployment that keeps nothing of that kind, and the method says so.
type Refusal ¶
type Refusal struct {
// Status is the HTTP status of the page: a 4xx, never a 5xx, which a CDN in
// front would replace with a page of its own.
Status int `json:"status"`
Summary string `json:"summary"`
// Detail is what Slack said.
Detail string `json:"detail,omitempty"`
// Reason is the short sentence the log and the audit record carry.
Reason string `json:"reason"`
Causes []string `json:"causes,omitempty"`
}
Refusal is a redirect that could not finish. It is told as data, since the words are the ones the person reads on the page.
type Service ¶
type Service struct {
// contains filtered or unexported fields
}
Service is the Slack module's console methods. It reads its Deps afresh at every call, so a deployment that connects a store late still serves it.
func NewService ¶
NewService is the service over deps, read at each call.
func (*Service) BeginSlackWorkspaceConnect ¶
func (c *Service) BeginSlackWorkspaceConnect( ctx context.Context, caller consolecall.Caller, req *directoryrosterv1.BeginSlackWorkspaceConnectRequest, ) (*directoryrosterv1.BeginSlackWorkspaceConnectResponse, error)
BeginSlackWorkspaceConnect creates the roster's App in a workspace, or prepares the reinstall of one created earlier, and returns Slack's authorize URL.
The configuration token is read from the request into one local, used for one call, and dropped: it is not written to the Secret or a record, not put into the signed state, not in any log line or audit record, and not in any error returned (errors from Slack never carry it).
func (*Service) ChangeSlackWorkspaceOwner ¶
func (c *Service) ChangeSlackWorkspaceOwner( ctx context.Context, caller consolecall.Caller, req *directoryrosterv1.ChangeSlackWorkspaceOwnerRequest, ) (*directoryrosterv1.ChangeSlackWorkspaceOwnerResponse, error)
ChangeSlackWorkspaceOwner changes the directory recorded as a connected workspace's owner, or removes it. The installation-wide operator alone: an owner that could hand its workspace to another, or take one, would make "who may operate this" a thing its operators decide.
func (*Service) CompleteCatalogue ¶
func (c *Service) CompleteCatalogue(ctx context.Context, caller consolecall.Caller, req CompleteRequest) (Completion, error)
CompleteCatalogue finishes a catalogue App's install: the console has verified where Slack sent the owner after installing it, and forwards the one-time code for the bot token.
The token is kept only if it belongs to the workspace the policy names for the App. Any other workspace is refused: the token is dropped without being stored, and the refusal is recorded.
func (*Service) CompleteWorkspace ¶
func (c *Service) CompleteWorkspace(ctx context.Context, caller consolecall.Caller, req CompleteRequest) (Completion, error)
CompleteWorkspace finishes a workspace's install: the console has verified where Slack sent the owner after installing the roster's App, and forwards the one-time code for the bot token.
The first install records which Slack team the workspace is. Every later install must belong to the same team; a token for any other is revoked and dropped without being stored, and the refusal is recorded.
func (*Service) ConfirmSlackRemovals ¶
func (c *Service) ConfirmSlackRemovals( ctx context.Context, caller consolecall.Caller, req *directoryrosterv1.ConfirmSlackRemovalsRequest, ) (*directoryrosterv1.ConfirmSlackRemovalsResponse, error)
ConfirmSlackRemovals lets exactly the removal set the operator saw go ahead. The fingerprint must be the one the latest report shows for that gate — the workspace's, or the named channel's: confirming a set that has since changed would confirm people nobody looked at.
func (*Service) CreateSlackApp ¶
func (c *Service) CreateSlackApp( ctx context.Context, caller consolecall.Caller, req *directoryrosterv1.CreateSlackAppRequest, ) (*directoryrosterv1.CreateSlackAppResponse, error)
CreateSlackApp creates a catalogue App in Slack.
The configuration token is read from the request into one local, used for one call, and dropped: it is not written to the Secret or a record, not put into the signed state, not in any log line or audit record, and not in any error returned (errors from Slack never carry it).
func (*Service) CreateSlackChannel ¶
func (c *Service) CreateSlackChannel( ctx context.Context, caller consolecall.Caller, req *directoryrosterv1.CreateSlackChannelRequest, ) (*directoryrosterv1.CreateSlackChannelResponse, error)
CreateSlackChannel puts a channel under management: operator of the workspace's owner, or of the installation.
func (*Service) CreateSlackSharedChannel ¶
func (c *Service) CreateSlackSharedChannel( ctx context.Context, caller consolecall.Caller, req *directoryrosterv1.CreateSlackSharedChannelRequest, ) (*directoryrosterv1.CreateSlackSharedChannelResponse, error)
CreateSlackSharedChannel defines a channel: operator of the host workspace's owner, or of the installation.
func (*Service) DeleteSlackChannel ¶
func (c *Service) DeleteSlackChannel( ctx context.Context, caller consolecall.Caller, req *directoryrosterv1.DeleteSlackChannelRequest, ) (*directoryrosterv1.DeleteSlackChannelResponse, error)
DeleteSlackChannel forgets the record. The channel stays in Slack, unless the request asks for it to be archived too.
func (*Service) DeleteSlackSharedChannel ¶
func (c *Service) DeleteSlackSharedChannel( ctx context.Context, caller consolecall.Caller, req *directoryrosterv1.DeleteSlackSharedChannelRequest, ) (*directoryrosterv1.DeleteSlackSharedChannelResponse, error)
DeleteSlackSharedChannel forgets the record. The channel stays in Slack.
func (*Service) DisconnectSlackWorkspace ¶
func (c *Service) DisconnectSlackWorkspace( ctx context.Context, caller consolecall.Caller, req *directoryrosterv1.DisconnectSlackWorkspaceRequest, ) (*directoryrosterv1.DisconnectSlackWorkspaceResponse, error)
DisconnectSlackWorkspace revokes the bot token and forgets the connection.
func (*Service) GetSlackStatus ¶
func (c *Service) GetSlackStatus( ctx context.Context, caller consolecall.Caller, _ *directoryrosterv1.GetSlackStatusRequest, ) (*directoryrosterv1.GetSlackStatusResponse, error)
GetSlackStatus is every Slack workspace the caller may view: the ones the policy declares, and any the controller reports on or a connection exists for that it no longer does (which only the installation-wide role sees, for want of an owner). A workspace's owner and team are the ones its connection recorded.
func (*Service) InstallSlackApp ¶
func (c *Service) InstallSlackApp( ctx context.Context, caller consolecall.Caller, req *directoryrosterv1.InstallSlackAppRequest, ) (*directoryrosterv1.InstallSlackAppResponse, error)
InstallSlackApp starts installing, or reinstalling, a created App: the response is Slack's authorize URL carrying signed state, and the header pins the flow to this browser.
func (*Service) ListSlackApps ¶
func (c *Service) ListSlackApps( ctx context.Context, caller consolecall.Caller, _ *directoryrosterv1.ListSlackAppsRequest, ) (*directoryrosterv1.ListSlackAppsResponse, error)
ListSlackApps is every declared App, and every App created from an entry no longer declared, with where each stands.
func (*Service) ListSlackChannels ¶
func (c *Service) ListSlackChannels( ctx context.Context, caller consolecall.Caller, _ *directoryrosterv1.ListSlackChannelsRequest, ) (*directoryrosterv1.ListSlackChannelsResponse, error)
ListSlackChannels is every record the caller may see, the channels the bots see that nothing manages, and the choices a form offers.
func (*Service) ListSlackSharedChannels ¶
func (c *Service) ListSlackSharedChannels( ctx context.Context, caller consolecall.Caller, _ *directoryrosterv1.ListSlackSharedChannelsRequest, ) (*directoryrosterv1.ListSlackSharedChannelsResponse, error)
ListSlackSharedChannels is every record the caller may see, with where the controller has each.
func (*Service) RequestSlackPass ¶
func (c *Service) RequestSlackPass( ctx context.Context, caller consolecall.Caller, req *directoryrosterv1.RequestSlackPassRequest, ) (*directoryrosterv1.RequestSlackPassResponse, error)
RequestSlackPass asks the Slack controller to pass over a workspace now. The request is a marker the controller notices within its poll (see the controller's watch); a second one under a minute after the first is refused, so the button cannot be leaned on.
func (*Service) RequireAnySlack ¶
func (c *Service) RequireAnySlack(ctx context.Context, caller consolecall.Caller, want access.Role) (access.Identity, Book, error)
RequireAnySlack is the gate on a page that lists Slack workspaces and then shows only the ones the caller may see: the installation-wide role, or the role over the directory that owns at least one Slack workspace, or the operator role over any directory when a declared workspace is still to be connected (the caller may be the one to connect it). A scoped role over a directory that owns none has nothing here to see, and is refused rather than shown an empty page that looks like an answer.
func (*Service) RequireSlack ¶
func (c *Service) RequireSlack(ctx context.Context, caller consolecall.Caller, want access.Role, workspace string) (access.Identity, error)
RequireSlack is consoleauthz.RequireOwner for one Slack workspace: the operator of its recorded owning directory, or the installation-wide role.
func (*Service) UpdateSlackChannel ¶
func (c *Service) UpdateSlackChannel( ctx context.Context, caller consolecall.Caller, req *directoryrosterv1.UpdateSlackChannelRequest, ) (*directoryrosterv1.UpdateSlackChannelResponse, error)
UpdateSlackChannel changes mode, ignore and sources. The caller's role is asked about the STORED workspace, which a request cannot change.
func (*Service) UpdateSlackSharedChannel ¶
func (c *Service) UpdateSlackSharedChannel( ctx context.Context, caller consolecall.Caller, req *directoryrosterv1.UpdateSlackSharedChannelRequest, ) (*directoryrosterv1.UpdateSlackSharedChannelResponse, error)
UpdateSlackSharedChannel changes with, from (directory groups) and private. The caller's role is asked about the STORED host, which a request cannot change.
func (*Service) WorkspaceStore ¶
func (c *Service) WorkspaceStore() (Workspaces, error)
WorkspaceStore is where connected workspaces are kept, or the refusal where the deployment keeps none.
type SharedRecords ¶
type SharedRecords interface {
// becomes (nil deletes it) and writes that under the object's version.
Apply(ctx context.Context, name string, decide func(current *reconcile.SharedChannel) (*reconcile.SharedChannel, error)) error
}
SharedRecords is where Slack Connect channel definitions are kept: the records the Slack controller reads.
type StatusReports ¶
StatusReports is what the Slack controller last reported, one document per workspace.
type Workspaces ¶
type Workspaces interface {
// Put writes a record and its credential.
Put(ctx context.Context, record connection.Record, credential connection.Credential) error
List(ctx context.Context) ([]connection.Record, error)
// Get is one workspace's record and credential; found is false when
// there is no credential.
Get(ctx context.Context, workspace string) (connection.Record, connection.Credential, bool, error)
// SetOwner changes only a record's owner, under the object's version;
// found is false when the workspace has no record.
SetOwner(ctx context.Context, workspace, owner string) (previous string, found bool, err error)
// RequestPass keeps an operator's request for a pass now; kept is false,
// with the time of the last request, when that was under a minute ago.
RequestPass(ctx context.Context, request connection.PassRequest) (kept bool, last time.Time, err error)
// PassRequests are the last request for each workspace.
PassRequests(ctx context.Context) (map[string]connection.PassRequest, error)
// Delete forgets a workspace and what belongs to it.
Delete(ctx context.Context, workspace string) error
PutConfirmation(ctx context.Context, confirmation connection.Confirmation) error
// Confirmations are every confirmation by connection.ConfirmationKey.
Confirmations(ctx context.Context) (map[string]connection.Confirmation, error)
}
Workspaces is where connected Slack workspaces are kept: each one's record, which the console shows, and credential, which only the Slack controller acts with. It also keeps the operators' confirmations of removal sets.