Skip to content

Commit

Permalink
first draft
Browse files Browse the repository at this point in the history
  • Loading branch information
or-else committed Jan 24, 2020
1 parent e803dc5 commit be6fbb8
Show file tree
Hide file tree
Showing 12 changed files with 156 additions and 22 deletions.
64 changes: 44 additions & 20 deletions docs/API.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
- [Logging in](#logging-in)
- [Changing Authentication Parameters](#changing-authentication-parameters)
- [Resetting a Password, i.e. "Forgot Password"](#resetting-a-password-ie-forgot-password)
- [Suspending a User](#suspending-a-user)
- [Credential Validation](#credential-validation)
- [Access Control](#access-control)
- [Topics](#topics)
Expand Down Expand Up @@ -145,20 +146,24 @@ When a connection is first established, the client application can send either a

Each user is assigned a unique ID. The IDs are composed as `usr` followed by base64-encoded 64-bit numeric value, e.g. `usr2il9suCbuko`. Users also have the following properties:

* created: timestamp when the user record was created
* updated: timestamp of when user's `public` was last updated
* username: unique string used in `basic` authentication; username is not accessible to other users
* defacs: object describing user's default access mode for peer to peer conversations with authenticated and anonymous users; see [Access control](#access-control) for details
* auth: default access mode for authenticated `auth` users
* anon: default access for anonymous `anon` users
* public: an application-defined object that describes the user. Anyone who can query user for `public` data.
* private: an application-defined object that is unique to the current user and accessible only by the user.
* tags: [discovery](#fnd-and-tags-finding-users-and-topics) and credentials.
* `created`: timestamp when the user record was created
* `updated`: timestamp of when user's `public` was last updated
* `status`: state of the account; two states are currently defined: `ok` and `suspended`
* `username`: unique string used in `basic` authentication; username is not accessible to other users
* `defacs`: object describing user's default access mode for peer to peer conversations with authenticated and anonymous users; see [Access control](#access-control) for details
* `auth`: default access mode for authenticated `auth` users
* `anon`: default access for anonymous `anon` users
* `public`: an application-defined object that describes the user. Anyone who can query user for `public` data.
* `private`: an application-defined object that is unique to the current user and accessible only by the user.
* `tags`: [discovery](#fnd-and-tags-finding-users-and-topics) and credentials.

A user account has a state. By default the state is `ok` which means the account is not restricted in any way and can be used normally. The other state is `suspended` which will prevent the user from accessing the account.

A user may maintain multiple simultaneous connections (sessions) with the server. Each session is tagged with a client-provided `User Agent` string intended to differentiate client software.

Logging out is not supported by design. If an application needs to change the user, it should open a new connection and authenticate it with the new user credentials.


### Authentication

Authentication is conceptually similar to [SASL](https://en.wikipedia.org/wiki/Simple_Authentication_and_Security_Layer): it's provided as a set of adapters each implementing a different authentication method. Authenticators are used during account registration [`{acc}`](#acc) and during [`{login}`](#login). The server comes with the following authentication methods out of the box:
Expand Down Expand Up @@ -223,6 +228,21 @@ where `jdoe@example.com` is an earlier validated user's email.

If the email matches the registration, the server will send a message using specified method and address with instructions for resetting the secret. The email contains a restricted security token which the user can include into an `{acc}` request with the new secret as described in [Changing Authentication Parameters](#changing-authentication-parameters).

### Suspending a User

User's account can be suspended by service administrator. Once the account is suspended, the user is no longer able to login and use the service.

Only the `root` user may suspend the account. To suspend the account the root user sends the following message:
```js
acc: {
id: "1a2b3", // string, client-provided message id, optional
user: "usr2il9suCbuko", // user being affected by the change
status: "suspended"
}
```
Sending the same message with `status: "ok"` un-suspends the account. A root user may check account status by executing `{get what="desc"}` command against user's `me` topic.


### Credential Validation

Server may be optionally configured to require validation of certain credentials associated with the user accounts and authentication scheme. For instance, it's possible to require user to provide a unique email or a phone number, or to solve a captcha as a condition of account registration.
Expand Down Expand Up @@ -263,19 +283,20 @@ Access permissions can be assigned on a per-user basis by `{set}` messages.
Topic is a named communication channel for one or more people. Topics have persistent properties. These topic properties can be queried by `{get what="desc"}` message.

Topic properties independent of the user making the query:
* created: timestamp of topic creation time
* updated: timestamp of when topic's `public` or `private` was last updated
* defacs: object describing topic's default access mode for authenticated and anonymous users; see [Access control](#access-control) for details
* auth: default access mode for authenticated users
* anon: default access for anonymous users
* seq: integer server-issued sequential ID of the latest `{data}` message sent through the topic
* public: an application-defined object that describes the topic. Anyone who can subscribe to topic can receive topic's `public` data.
* `created`: timestamp of topic creation time
* `updated`: timestamp of when topic's `public` or `private` was last updated
* `touched`: timestamp of the last message sent to the topic
* `defacs`: object describing topic's default access mode for authenticated and anonymous users; see [Access control](#access-control) for details
* `auth`: default access mode for authenticated users
* `anon`: default access for anonymous users
* `seq`: integer server-issued sequential ID of the latest `{data}` message sent through the topic
* `public`: an application-defined object that describes the topic. Anyone who can subscribe to topic can receive topic's `public` data.

User-dependent topic properties:
* acs: object describing given user's current access permissions; see [Access control](#access-control) for details
* want: access permission requested by this user
* given: access permissions given to this user
* private: an application-defined object that is unique to the current user.
* `acs`: object describing given user's current access permissions; see [Access control](#access-control) for details
* `want`: access permission requested by this user
* `given`: access permissions given to this user
* `private`: an application-defined object that is unique to the current user.

Topic usually have subscribers. One of the subscribers may be designated as topic owner (`O` access permission) with full access permissions. The list of subscribers can be queries with a `{get what="sub"}` message. The list of subscribers is returned in a `sub` section of a `{meta}` message.

Expand Down Expand Up @@ -596,6 +617,7 @@ acc: {
user: "new", // string, "new" to create a new user, default: current user, optional
token: "XMgS...8+BO0=", // string, authentication token to use for the request if the
// session is not authenticated, optional
status: "ok", // change user's status; no default value, optional.
scheme: "basic", // authentication scheme for this account, required;
// "basic" and "anon" are currently supported for account creation.
secret: base64encode("username:password"), // string, base64 encoded secret for the chosen
Expand Down Expand Up @@ -1082,6 +1104,8 @@ meta: {
desc: {
created: "2015-10-24T10:26:09.716Z",
updated: "2015-10-24T10:26:09.716Z",
status: "ok", // account status; included for `me` topic only, and only if
// the request is sent by a root-authenticated session.
defacs: { // topic's default access permissions; present only if the current
//user has 'S' permission
auth: "JRWP", // default access for authenticated users
Expand Down
3 changes: 3 additions & 0 deletions pbx/model.proto
Original file line number Diff line number Diff line change
Expand Up @@ -164,6 +164,8 @@ message ClientAcc {
repeated ClientCred cred = 8;
// Authentication token used for resetting a password.
bytes token = 9;
// Account state: normal, suspended
string state = 10;
}

// Login {login} message
Expand Down Expand Up @@ -304,6 +306,7 @@ message TopicDesc {
int32 del_id = 9;
bytes public = 10;
bytes private = 11;
string state = 12;
}

// MsgTopicSub: topic subscription details, sent in Meta message
Expand Down
12 changes: 12 additions & 0 deletions server/auth/basic/auth_basic.go
Original file line number Diff line number Diff line change
Expand Up @@ -222,6 +222,18 @@ func (a *authenticator) Authenticate(secret []byte) (*auth.Rec, []byte, error) {
return nil, nil, types.ErrFailed
}

// Check if user's account is suspended.
user, err := store.Users.Get(uid)
if err != nil {
return nil, nil, err
}
if user == nil {
return nil, nil, types.ErrNotFound
}
if user.State != types.UserStateOK {
return nil, nil, types.ErrPermissionDenied
}

var lifetime time.Duration
if !expires.IsZero() {
lifetime = time.Until(expires)
Expand Down
3 changes: 3 additions & 0 deletions server/auth/rest/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,8 @@ Request and response payloads are formatted as JSON. Some of the request or resp
"boolval": true, // boolean value, optional
"strarr": ["abc", "def"], // array of strings, optoional
"newacc": { // data to use for creating a new account.
// Optional account state.
state: "ok",
// Default access mode
"auth": "JRWPS",
"anon": "N",
Expand Down Expand Up @@ -185,6 +187,7 @@ The server may optionally return a challenge as `byteval`.
"tags": ["email:alice@example.com", "uname:alice"]
},
"newacc": {
"state": "suspended",
"auth": "JRWPS",
"anon": "N",
"public": {/* see /docs/API.md#public-and-private-fields */},
Expand Down
9 changes: 9 additions & 0 deletions server/auth/rest/auth_rest.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,8 @@ type request struct {

// User initialization data when creating a new user.
type newAccount struct {
// Account state: normal as "ok" of "suspended".
State string `json:"state,omitempty"`
// Default access mode
Auth string `json:"auth,omitempty"`
Anon string `json:"anon,omitempty"`
Expand Down Expand Up @@ -183,6 +185,13 @@ func (a *authenticator) Authenticate(secret []byte) (*auth.Rec, []byte, error) {
Public: resp.NewAcc.Public,
Tags: resp.Record.Tags,
}

if resp.NewAcc.State != "" {
if _, err := user.SetState(resp.NewAcc.State); err != nil {
return nil, nil, err
}
}

user.Access.Auth.UnmarshalText([]byte(resp.NewAcc.Auth))
user.Access.Anon.UnmarshalText([]byte(resp.NewAcc.Anon))
_, err = store.Users.Create(&user, resp.NewAcc.Private)
Expand Down
5 changes: 5 additions & 0 deletions server/datamodel.go
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,8 @@ type MsgClientAcc struct {
Id string `json:"id,omitempty"`
// "newXYZ" to create a new user or UserId to update a user; default: current user.
User string `json:"user,omitempty"`
// Account state: normal, suspended.
State string `json:"status,omitempty"`
// Authentication level of the user when UserID is set and not equal to the current user.
// Either "", "auth" or "anon". Default: ""
AuthLevel string
Expand Down Expand Up @@ -353,6 +355,9 @@ type MsgTopicDesc struct {
// Timestamp of the last message
TouchedAt *time.Time `json:"touched,omitempty"`

// Account state, 'me' topic only.
State string `json:"state,omitempty"`

// If the group topic is online.
Online bool `json:"online,omitempty"`

Expand Down
5 changes: 3 additions & 2 deletions server/db/mysql/adapter.go
Original file line number Diff line number Diff line change
Expand Up @@ -604,10 +604,11 @@ func (a *adapter) UserCreate(user *t.User) error {
}()

decoded_uid := store.DecodeUid(user.Uid())
if _, err = tx.Exec("INSERT INTO users(id,createdat,updatedat,access,public,tags) VALUES(?,?,?,?,?,?)",
if _, err = tx.Exec("INSERT INTO users(id,createdat,updatedat,state,access,public,tags) VALUES(?,?,?,?,?,?,?)",
decoded_uid,
user.CreatedAt, user.UpdatedAt,
user.Access, toJSON(user.Public), user.Tags); err != nil {
user.State, user.Access,
toJSON(user.Public), user.Tags); err != nil {
return err
}

Expand Down
4 changes: 4 additions & 0 deletions server/hub.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
package main

import (
"github.com/tinode/chat/github.com/tinode/chat/server/auth"
"container/list"
"log"
"strings"
Expand Down Expand Up @@ -562,6 +563,9 @@ func replyOfflineTopicGetDesc(sess *Session, topic string, msg *ClientComMessage
desc.CreatedAt = &suser.CreatedAt
desc.UpdatedAt = &suser.UpdatedAt
desc.Public = suser.Public
if sess.authLvl = auth.LevelRoot {
desc.State = suser.GetState()
}
}

sub, err := store.Subs.Get(topic, asUid)
Expand Down
4 changes: 4 additions & 0 deletions server/pbconverter.go
Original file line number Diff line number Diff line change
Expand Up @@ -226,6 +226,7 @@ func pbCliSerialize(msg *ClientComMessage) *pbx.ClientMsg {
pkt.Message = &pbx.ClientMsg_Acc{Acc: &pbx.ClientAcc{
Id: msg.Acc.Id,
UserId: msg.Acc.User,
State: msg.Acc.State,
Token: msg.Acc.Token,
Scheme: msg.Acc.Scheme,
Secret: msg.Acc.Secret,
Expand Down Expand Up @@ -323,6 +324,7 @@ func pbCliDeserialize(pkt *pbx.ClientMsg) *ClientComMessage {
msg.Acc = &MsgClientAcc{
Id: acc.GetId(),
User: acc.GetUserId(),
State: acc.GetState(),
Scheme: acc.GetScheme(),
Secret: acc.GetSecret(),
Login: acc.GetLogin(),
Expand Down Expand Up @@ -714,6 +716,7 @@ func pbTopicDescSerialize(desc *MsgTopicDesc) *pbx.TopicDesc {
CreatedAt: timeToInt64(desc.CreatedAt),
UpdatedAt: timeToInt64(desc.UpdatedAt),
TouchedAt: timeToInt64(desc.TouchedAt),
State: desc.State,
Defacs: pbDefaultAcsSerialize(desc.DefaultAcs),
Acs: pbAccessModeSerialize(desc.Acs),
SeqId: int32(desc.SeqId),
Expand All @@ -733,6 +736,7 @@ func pbTopicDescDeserialize(desc *pbx.TopicDesc) *MsgTopicDesc {
CreatedAt: int64ToTime(desc.GetCreatedAt()),
UpdatedAt: int64ToTime(desc.GetUpdatedAt()),
TouchedAt: int64ToTime(desc.GetTouchedAt()),
State: desc.GetState(),
DefaultAcs: pbDefaultAcsDeserialize(desc.GetDefacs()),
Acs: pbAccessModeDeserialize(desc.GetAcs()),
SeqId: int(desc.SeqId),
Expand Down
36 changes: 36 additions & 0 deletions server/store/types/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,8 @@ const (
ErrPermissionDenied = StoreError("denied")
// ErrInvalidResponse means the client's response does not match server's expectation.
ErrInvalidResponse = StoreError("invalid response")
// ErrDisabled means access to resource is disabled.
ErrPermissionDisabled = StoreError("disabled")
)

// Uid is a database-specific record id, suitable to be used as a primary key.
Expand Down Expand Up @@ -400,6 +402,40 @@ type User struct {
DeviceArray []*DeviceDef `json:"-" bson:"devices"`
}

type UserState int

func (us UserState) String() {
switch us {
case UserStateOK:
return "ok"
case UserStateSuspended:
return "suspended"
}
return ""
}

const (
UserStateOK UserState = 0
UserStateSuspended UserState = 1
)

func (u *User) SetState(state string) (int, error) {
var err error
switch state {
case "ok":
u.State = int(UserStateOK)
case "suspended":
u.State = int(UserStateSuspended)
default:
err = errors.New("invalide account state")
}
return u.State, err
}

func (u *User) GetState() string {
return UserState(u.State).String()
}

// AccessMode is a definition of access mode bits.
type AccessMode uint

Expand Down
3 changes: 3 additions & 0 deletions server/topic.go
Original file line number Diff line number Diff line change
Expand Up @@ -1417,6 +1417,9 @@ func (t *Topic) replyGetDesc(sess *Session, asUid types.Uid, id string, opts *Ms
Want: pud.modeWant.String(),
Given: pud.modeGiven.String(),
Mode: (pud.modeGiven & pud.modeWant).String()}
} else if sess.authLvl == auth.LevelRoot {
// If 'me' is in memory then user account is invariable not suspended.
desc.State = types.UserStateOK.String()
}

if t.cat == types.TopicCatGrp && (pud.modeGiven & pud.modeWant).IsPresencer() {
Expand Down
30 changes: 30 additions & 0 deletions server/user.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,23 @@ func replyCreateUser(s *Session, msg *ClientComMessage, rec *auth.Rec) {
var user types.User
var private interface{}

// If account state is being assigned, make sure the sender is a root user.
if msg.Acc.State != "" {
if msg.authLvl != auth.LevelRoot {
log.Println("create user: attempt to set account state by non-root", s.sid)
msg := ErrPermissionDenied(msg.id, "", msg.timestamp)
msg.Ctrl.Params = map[string]interface{}{"what": "state"}
s.queueOut(msg)
return
}

if _, err := user.SetState(msg.Acc.State); err != nil {
log.Println("create user: invalid account state", err, s.sid)
s.queueOut(ErrMalformed(msg.id, "", msg.timestamp))
return
}
}

// Ensure tags are unique and not restricted.
if tags := normalizeTags(msg.Acc.Tags); tags != nil {
if !restrictedTagsEqual(tags, nil, globals.immutableTagNS) {
Expand Down Expand Up @@ -209,6 +226,12 @@ func replyUpdateUser(s *Session, msg *ClientComMessage, rec *auth.Rec) {
return
}

if msg.Acc.State != "" && authLvl != auth.LevelRoot {
s.queueOut(ErrPermissionDenied(msg.id, "", msg.timestamp))
log.Println("replyUpdateUser: attempt to change account state by non-root", s.sid)
return
}

user, err := store.Users.Get(uid)
if user == nil && err == nil {
err = types.ErrNotFound
Expand Down Expand Up @@ -242,6 +265,13 @@ func replyUpdateUser(s *Session, msg *ClientComMessage, rec *auth.Rec) {
}
}
}
} else if msg.Acc.State != "" {
if _, err2 := user.SetState(msg.Acc.State); err2 != nil {
log.Println("replyUpdateUser: invalid account state", s.sid)
err = types.ErrMalformed
} else {
err = store.Users.Update(uid, map[string]interface{}{"State": user.State})
}
} else {
err = types.ErrMalformed
}
Expand Down

0 comments on commit be6fbb8

Please sign in to comment.