Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix a lot of things about sharing #1130

Merged
merged 12 commits into from
Jan 2, 2018
2 changes: 0 additions & 2 deletions assets/templates/authorize_sharing.html
Original file line number Diff line number Diff line change
Expand Up @@ -55,14 +55,12 @@ <h1>{{t "Authorize Sharing Title" .Client.ClientName}}</h1>
</div>
<footer>
<div class="controls">
<button type="cancel" class="btn btn-secondary">{{t "Authorize Cancel Sharing"}}</button>
<button type="submit" class="btn btn-primary">{{t "Authorize Submit Sharing"}}</button>
</div>
</footer>
</form>
</div>
</section>
</main>
<script src="{{asset .Domain "/scripts/cancel-button.js"}}"></script>
</body>
</html>
2 changes: 1 addition & 1 deletion docs/permissions.md
Original file line number Diff line number Diff line change
Expand Up @@ -231,7 +231,7 @@ associated to the sent token.
#### Request

```http
POST /permissions?codes=bob,jane&ttl=1d HTTP/1.1
POST /permissions?codes=bob,jane&ttl=1D HTTP/1.1
Host: cozy.example.net
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ
Content-Type: application/vnd.api+json
Expand Down
53 changes: 38 additions & 15 deletions pkg/permissions/permissions.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,13 +13,15 @@ import (
// Permission is a storable object containing a set of rules and
// several codes
type Permission struct {
PID string `json:"_id,omitempty"`
PRev string `json:"_rev,omitempty"`
Type string `json:"type,omitempty"`
SourceID string `json:"source_id,omitempty"`
Permissions Set `json:"permissions,omitempty"`
ExpiresAt int `json:"expires_at,omitempty"`
Codes map[string]string `json:"codes,omitempty"`
PID string `json:"_id,omitempty"`
PRev string `json:"_rev,omitempty"`
Type string `json:"type,omitempty"`
SourceID string `json:"source_id,omitempty"`
Permissions Set `json:"permissions,omitempty"`

// XXX omitempty does not work for time.Time, thus the interface{} type
ExpiresAt interface{} `json:"expires_at,omitempty"`
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why not using a *time.Time ?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good question. I have copied what was done here: https://github.com/cozy/cozy-stack/blob/master/pkg/oauth/client.go#L63

Codes map[string]string `json:"codes,omitempty"`
}

const (
Expand Down Expand Up @@ -74,6 +76,14 @@ func (p *Permission) SetID(id string) { p.PID = id }
// SetRev implements jsonapi.Doc
func (p *Permission) SetRev(rev string) { p.PRev = rev }

// Expired returns true if the permissions are no longer valid
func (p *Permission) Expired() bool {
if p.ExpiresAt == nil {
return false
}
return p.ExpiresAt.(time.Time).Before(time.Now())
}

// AddRules add some rules to the permission doc
func (p *Permission) AddRules(rules ...Rule) {
newperms := append(p.Permissions, rules...)
Expand Down Expand Up @@ -112,9 +122,14 @@ func (p *Permission) ParentOf(child *Permission) bool {

// GetByID fetch a permission by its ID
func GetByID(db couchdb.Database, id string) (*Permission, error) {
var perm Permission
err := couchdb.GetDoc(db, consts.Permissions, id, &perm)
return &perm, err
perm := &Permission{}
if err := couchdb.GetDoc(db, consts.Permissions, id, perm); err != nil {
return nil, err
}
if perm.Expired() {
return nil, ErrExpiredToken
}
return perm, nil
}

// GetForRegisterToken create a non-persisted permissions doc with hard coded
Expand Down Expand Up @@ -206,7 +221,11 @@ func getFromSource(db couchdb.Database, permType, docType, slug string) (*Permis
if len(res) == 0 {
return nil, fmt.Errorf("no permission doc for %v", slug)
}
return &res[0], nil
perm := &res[0]
if perm.Expired() {
return nil, ErrExpiredToken
}
return perm, nil
}

// GetForShareCode retrieves the Permission doc for a given sharing code
Expand All @@ -228,13 +247,16 @@ func GetForShareCode(db couchdb.Database, tokenCode string) (*Permission, error)
return nil, fmt.Errorf("Bad state: several permission docs for token %v", tokenCode)
}

var pdoc Permission
err = json.Unmarshal(res.Rows[0].Doc, &pdoc)
perm := &Permission{}
err = json.Unmarshal(res.Rows[0].Doc, perm)
if err != nil {
return nil, err
}

return &pdoc, nil
if perm.Expired() {
return nil, ErrExpiredToken
}
return perm, nil
}

// CreateWebappSet creates a Permission doc for an app
Expand Down Expand Up @@ -296,7 +318,7 @@ func updateAppSet(db couchdb.Database, doc *Permission, typ, docType, slug strin
}

// CreateShareSet creates a Permission doc for sharing by link
func CreateShareSet(db couchdb.Database, parent *Permission, codes map[string]string, set Set) (*Permission, error) {
func CreateShareSet(db couchdb.Database, parent *Permission, codes map[string]string, set Set, expiresAt interface{}) (*Permission, error) {
if parent.Type != TypeWebapp && parent.Type != TypeKonnector && parent.Type != TypeOauth {
return nil, ErrOnlyAppCanCreateSubSet
}
Expand All @@ -321,6 +343,7 @@ func CreateShareSet(db couchdb.Database, parent *Permission, codes map[string]st
SourceID: parent.SourceID,
Permissions: set,
Codes: codes,
ExpiresAt: expiresAt,
}

err := couchdb.CreateDoc(db, doc)
Expand Down
2 changes: 1 addition & 1 deletion pkg/permissions/permissions_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -298,7 +298,7 @@ func TestSubset(t *testing.T) {
func TestCreateShareSetBlacklist(t *testing.T) {
s := Set{Rule{Type: "io.cozy.notifications"}}
parent := &Permission{Type: TypeWebapp, Permissions: s}
_, err := CreateShareSet(nil, parent, nil, s)
_, err := CreateShareSet(nil, parent, nil, s, nil)
assert.Error(t, err)
e, ok := err.(*echo.HTTPError)
assert.True(t, ok)
Expand Down
29 changes: 1 addition & 28 deletions pkg/sharings/member.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import (
// Member contains the information about a recipient (or the sharer) for a sharing
type Member struct {
Status string `json:"status,omitempty"`
URL string `json:"url,omitempty"` // TODO check that this URL is well filled
URL string `json:"url,omitempty"`

// Only a reference on the contact is persisted in the sharing document
RefContact couchdb.DocReference `json:"contact,omitempty"`
Expand Down Expand Up @@ -156,30 +156,3 @@ func (m *Member) RegisterClient(i *instance.Instance, u *url.URL) error {
m.Client = *resClient
return nil
}

// RecipientInfo describes the recipient information that will be transmitted to
// the sharing workers.
type RecipientInfo struct {
Domain string
Scheme string
Client auth.Client
AccessToken auth.AccessToken
}

// ExtractRecipientInfo returns a RecipientInfo from a Member
func ExtractRecipientInfo(m *Member) (*RecipientInfo, error) {
if m.URL == "" {
return nil, ErrRecipientHasNoURL
}
u, err := url.Parse(m.URL)
if err != nil {
return nil, err
}
info := RecipientInfo{
Domain: u.Host,
Scheme: u.Scheme,
AccessToken: m.AccessToken,
Client: m.Client,
}
return &info, nil
}
85 changes: 85 additions & 0 deletions pkg/sharings/oauth.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
package sharings

import (
"net/http"
"net/url"

"github.com/cozy/cozy-stack/client/auth"
"github.com/cozy/cozy-stack/client/request"
"github.com/cozy/cozy-stack/pkg/couchdb"
"github.com/cozy/cozy-stack/pkg/instance"
)

// RecipientInfo describes the recipient information that will be transmitted to
// the sharing workers.
type RecipientInfo struct {
Domain string
Scheme string
Client auth.Client
AccessToken auth.AccessToken
}

// ExtractRecipientInfo returns a RecipientInfo from a Member
func ExtractRecipientInfo(m *Member) (*RecipientInfo, error) {
if m.URL == "" {
return nil, ErrRecipientHasNoURL
}
u, err := url.Parse(m.URL)
if err != nil {
return nil, err
}
info := RecipientInfo{
Domain: u.Host,
Scheme: u.Scheme,
AccessToken: m.AccessToken,
Client: m.Client,
}
return &info, nil
}

// RefreshTokenAndRetry is called after an authentication failure.
// It tries to renew the access_token and request again
func RefreshTokenAndRetry(ins *instance.Instance, sharingID string, info *RecipientInfo, opts *request.Options) (*http.Response, error) {
req := &auth.Request{
Domain: opts.Domain,
Scheme: opts.Scheme,
}
sharing, err := FindSharing(ins, sharingID)
if err != nil {
return nil, err
}
var m *Member
if sharing.Owner {
m, err = sharing.GetMemberFromClientID(ins, info.Client.ClientID)
if err != nil {
return nil, err
}
} else {
if sharing.Sharer.Client.ClientID != info.Client.ClientID {
return nil, ErrRecipientDoesNotExist
}
m = sharing.Sharer
}
refreshToken := info.AccessToken.RefreshToken
access, err := req.RefreshToken(&info.Client, &info.AccessToken)
if err != nil {
ins.Logger().Errorf("[sharing] Refresh token request failed: %v", err)
return nil, err
}
access.RefreshToken = refreshToken
m.AccessToken = *access
if err = couchdb.UpdateDoc(ins, sharing); err != nil {
return nil, err
}
opts.Headers["Authorization"] = "Bearer " + access.AccessToken
res, err := request.Req(opts)
return res, err
}

// IsAuthError returns true if the given error is an authentication one
func IsAuthError(err error) bool {
if v, ok := err.(*request.Error); ok {
return v.Title == "Bad Request" || v.Title == "Unauthorized"
}
return false
}
Loading