code.oscarkilo.com/okg/who/who.go

..
username.go
username_test.go
who.go
who_test.go
// Package who is the public HTTP client for OscarKilo's //who
// service. It is used by `okg` and any other tool that talks
// to oscarkilo.com over the network.
//
// The struct here is named HTTPClient (not just Client) so
// applications can wrap it in their own Client abstractions
// without name collision.
package who

import "time"

import "oscarkilo.com/okg/internal/rest"

// HTTPClient talks to a //who server over HTTPS. Embeds the
// shared rest helpers; who-specific methods live below.
type HTTPClient struct {
  *rest.Client
}

func NewHTTPClient(host, apiKey string) *HTTPClient {
  return &HTTPClient{Client: rest.NewClient(host, apiKey)}
}

// ---- Common types ----

// AppDataEntry is one user-scoped app-data record served by
// //who (per-user key/value, namespaced by app).
type AppDataEntry struct {
  App       string    `json:"app"`
  Key       string    `json:"key"`
  Value     string    `json:"value"`
  Timestamp time.Time `json:"timestamp"`
}

// ---- Caller identity ----

// Profile is the caller's //who profile. Mirrors the
// JSON-serialized subset of //who.Profile that GET
// /login/profile/get returns. ApiKeys, Logins, and timestamps
// are not exposed here yet — add when okg has a use.
type Profile struct {
  Owid        string         `json:"owid"`
  Username    string         `json:"username"`
  Name        string         `json:"name"`
  Email       string         `json:"email"`
  PortraitUrl string         `json:"portrait_url"`
  OwnerOwid   string         `json:"owner_owid"`
  Groups      []string       `json:"groups"`
  AppData     []AppDataEntry `json:"app_data,omitempty"`
}

// GetProfile returns the caller's own profile, identified by
// the Bearer key.
func (c *HTTPClient) GetProfile() (*Profile, error) {
  var p Profile
  if err := c.GetJSON("/login/profile/get", &p); err != nil {
    return nil, err
  }
  return &p, nil
}

// ---- Groups ----

// Group describes a //who group (a named entity with members).
// Matches the row shape in /groups/list's response.
type Group struct {
  Owid          string `json:"owid"`
  Username      string `json:"username"`
  Name          string `json:"name"`
  OwnerOwid     string `json:"owner_owid"`
  OwnerUsername string `json:"owner_username"`
}

type listGroupsResponse struct {
  Groups []Group `json:"groups"`
}

// ListGroups returns the groups visible to the caller.
func (c *HTTPClient) ListGroups() ([]Group, error) {
  var res listGroupsResponse
  if err := c.GetJSON("/groups/list", &res); err != nil {
    return nil, err
  }
  return res.Groups, nil
}

// CreateGroupRequest is the body for POST /groups/add. Username
// is the group's //who username (e.g. "team"); Name is the
// human-facing display name; OwnerUsername is the user who'll
// own the new group.
type CreateGroupRequest struct {
  Username      string `json:"username"`
  Name          string `json:"name"`
  OwnerUsername string `json:"owner_username"`
}

// CreateGroup creates a new //who group.
func (c *HTTPClient) CreateGroup(req CreateGroupRequest) error {
  return c.PostJSON("/groups/add", req, nil)
}

// JoinGroupsRequest is the body for POST /groups/join. The
// server adds every (GroupUsernames[i], MemberUsernames[j])
// pair, requiring the caller to be an owner of each group.
type JoinGroupsRequest struct {
  GroupUsernames  []string `json:"group_usernames"`
  MemberUsernames []string `json:"member_usernames"`
}

// JoinGroups adds members to groups (all pairwise combinations).
func (c *HTTPClient) JoinGroups(req JoinGroupsRequest) error {
  return c.PostJSON("/groups/join", req, nil)
}

// GroupMembersResponse is the response from POST
// /groups/members. Up lists the groups that contain the queried
// entity (each level as a slice of owids). Down lists its
// members the same way. Usernames maps every owid mentioned to
// its //who username.
type GroupMembersResponse struct {
  Up        [][]string        `json:"up"`
  Down      [][]string        `json:"down"`
  Usernames map[string]string `json:"usernames"`
}

// GroupMembers returns membership DAG navigation for the given
// entity owid. For a group, Down enumerates its members.
func (c *HTTPClient) GroupMembers(
  ownerOwid string,
) (*GroupMembersResponse, error) {
  var res GroupMembersResponse
  err := c.PostJSON("/groups/members",
    map[string]string{"owid": ownerOwid}, &res)
  if err != nil {
    return nil, err
  }
  return &res, nil
}

// LeaveGroupRequest is the body for POST /groups/leave. Both
// IDs are owids (obfuscated wids); to translate from usernames,
// call ListGroups + GroupMembers first.
type LeaveGroupRequest struct {
  GroupOwid  string `json:"group_owid"`
  MemberOwid string `json:"member_owid"`
}

// LeaveGroup removes a member from a group. The caller must own
// the group.
func (c *HTTPClient) LeaveGroup(req LeaveGroupRequest) error {
  return c.PostJSON("/groups/leave", req, nil)
}

// DeleteGroupRequest is the body for POST /groups/delete. Owid
// is the group's obfuscated wid.
type DeleteGroupRequest struct {
  Owid string `json:"owid"`
}

// DeleteGroup destroys a group. The caller must own it (or be a
// //who admin).
func (c *HTTPClient) DeleteGroup(req DeleteGroupRequest) error {
  return c.PostJSON("/groups/delete", req, nil)
}

// ---- Authz ----

// User is the public //who-user identifier. It carries only
// the fields safe to expose to external callers.
type User struct {
  Owid     string         `json:"owid"`
  Username string         `json:"username"`
  AppData  []AppDataEntry `json:"appdata,omitempty"`
}

// AuthzEntry is one row of GET /authz/list: the URI, its owner
// and reader, and the caller's effective rights on it.
type AuthzEntry struct {
  Uri      string `json:"uri"`
  Owner    *User  `json:"owner"`
  Reader   *User  `json:"reader"`
  IsOwner  bool   `json:"is_owner"`
  IsReader bool   `json:"is_reader"`
}

type listAuthzResponse struct {
  Uris []AuthzEntry `json:"uris"`
}

// ListAuthz returns all authz URIs visible to the caller.
func (c *HTTPClient) ListAuthz() ([]AuthzEntry, error) {
  var res listAuthzResponse
  if err := c.GetJSON("/authz/list", &res); err != nil {
    return nil, err
  }
  return res.Uris, nil
}

// AuthzSetRequest is the body for POST /authz/set. Both
// usernames must be non-empty and known to //who; the public
// endpoint does not honor the "anyone" sentinel.
type AuthzSetRequest struct {
  Uri            string `json:"uri"`
  OwnerUsername  string `json:"owner_username"`
  ReaderUsername string `json:"reader_username"`
}

// SetAuthzMulti writes one or more authz entries atomically.
// Either every entry's write succeeds or none do (server-side
// Pebble batch). The caller must be an owner of every URI in
// the batch (or a //who admin).
func (c *HTTPClient) SetAuthzMulti(
  reqs []AuthzSetRequest,
) error {
  return c.PostJSON("/authz/set", reqs, nil)
}

// SetAuthz writes one authz entry. Syntactic sugar over
// SetAuthzMulti for the single-entry case.
func (c *HTTPClient) SetAuthz(req AuthzSetRequest) error {
  return c.SetAuthzMulti([]AuthzSetRequest{req})
}

// AuthzDeleteRequest is the body for POST /authz/delete.
type AuthzDeleteRequest struct {
  Uri string `json:"uri"`
}

// DeleteAuthz removes an authz entry. The caller must own the URI.
func (c *HTTPClient) DeleteAuthz(req AuthzDeleteRequest) error {
  return c.PostJSON("/authz/delete", req, nil)
}