From d65cefcac75b53c41777fcf758454ef2224fda0a Mon Sep 17 00:00:00 2001 From: Yaroslav Vorobiov Date: Thu, 27 Dec 2018 17:30:15 +0200 Subject: [PATCH] Satellite api keys api (#936) --- pkg/satellite/apikeys.go | 61 +++++-- pkg/satellite/projectmembers.go | 4 +- pkg/satellite/satellitedb/apikeys.go | 31 ++-- pkg/satellite/satellitedb/dbx/satellitedb.dbx | 4 - .../satellitedb/dbx/satellitedb.dbx.go | 75 +------- pkg/satellite/satellitedb/projectmembers.go | 6 +- .../satellitedb/projectmembers_test.go | 12 +- .../satelliteweb/satelliteql/apikey.go | 59 ++++++ .../satelliteweb/satelliteql/mutation.go | 71 +++++++- .../satelliteweb/satelliteql/project.go | 9 + .../satelliteweb/satelliteql/typecreator.go | 25 +++ pkg/satellite/service.go | 170 ++++++++++++++---- 12 files changed, 384 insertions(+), 143 deletions(-) create mode 100644 pkg/satellite/satelliteweb/satelliteql/apikey.go diff --git a/pkg/satellite/apikeys.go b/pkg/satellite/apikeys.go index 3ae970810..2ed8bd009 100644 --- a/pkg/satellite/apikeys.go +++ b/pkg/satellite/apikeys.go @@ -4,35 +4,72 @@ package satellite import ( + "bytes" "context" + "crypto/rand" + "encoding/base64" + "io" "time" + "github.com/zeebo/errs" + "github.com/skyrings/skyring-common/tools/uuid" ) -// APIKeys is interface for working with apikeys store +// APIKeys is interface for working with api keys store type APIKeys interface { // GetByProjectID retrieves list of APIKeys for given projectID - GetByProjectID(ctx context.Context, projectID uuid.UUID) ([]APIKey, error) - // Get retrieves APIKey with given ID - Get(ctx context.Context, id uuid.UUID) (*APIKey, error) - // Create creates and stores new APIKey - Create(ctx context.Context, key APIKey) (*APIKey, error) - // Update updates APIKey in store - Update(ctx context.Context, key APIKey) error - // Delete deletes APIKey from store + GetByProjectID(ctx context.Context, projectID uuid.UUID) ([]APIKeyInfo, error) + // Get retrieves APIKeyInfo with given ID + Get(ctx context.Context, id uuid.UUID) (*APIKeyInfo, error) + // Create creates and stores new APIKeyInfo + Create(ctx context.Context, key APIKey, info APIKeyInfo) (*APIKeyInfo, error) + // Update updates APIKeyInfo in store + Update(ctx context.Context, key APIKeyInfo) error + // Delete deletes APIKeyInfo from store Delete(ctx context.Context, id uuid.UUID) error } -// APIKey describing apikey model in the database -type APIKey struct { +// APIKeyInfo describing api key model in the database +type APIKeyInfo struct { ID uuid.UUID `json:"id"` // Fk on project ProjectID uuid.UUID `json:"projectId"` - Key []byte `json:"key"` Name string `json:"name"` CreatedAt time.Time `json:"createdAt"` } + +// APIKey is an api key type +type APIKey [24]byte + +// String implements Stringer +func (key APIKey) String() string { + emptyKey := APIKey{} + if bytes.Equal(key[:], emptyKey[:]) { + return "" + } + + return base64.URLEncoding.EncodeToString(key[:]) +} + +// APIKeyFromBytes creates new key from byte slice +func APIKeyFromBytes(b []byte) *APIKey { + key := new(APIKey) + copy(key[:], b) + return key +} + +// createAPIKey creates new api key +func createAPIKey() (*APIKey, error) { + key := new(APIKey) + + n, err := io.ReadFull(rand.Reader, key[:]) + if err != nil || n != 24 { + return nil, errs.New("error creating api key") + } + + return key, nil +} diff --git a/pkg/satellite/projectmembers.go b/pkg/satellite/projectmembers.go index 2299520aa..f12b6dd13 100644 --- a/pkg/satellite/projectmembers.go +++ b/pkg/satellite/projectmembers.go @@ -12,8 +12,8 @@ import ( // ProjectMembers exposes methods to manage ProjectMembers table in database. type ProjectMembers interface { - // GetByMemberID is a method for querying project member from the database by memberID. - GetByMemberID(ctx context.Context, memberID uuid.UUID) (*ProjectMember, error) + // GetByMemberID is a method for querying project members from the database by memberID. + GetByMemberID(ctx context.Context, memberID uuid.UUID) ([]ProjectMember, error) // GetByProjectID is a method for querying project members from the database by projectID, offset and limit. GetByProjectID(ctx context.Context, projectID uuid.UUID, limit int, offset int64) ([]ProjectMember, error) // Insert is a method for inserting project member into the database. diff --git a/pkg/satellite/satellitedb/apikeys.go b/pkg/satellite/satellitedb/apikeys.go index 18b15939a..5bef72023 100644 --- a/pkg/satellite/satellitedb/apikeys.go +++ b/pkg/satellite/satellitedb/apikeys.go @@ -19,23 +19,23 @@ type apikeys struct { } // GetByProjectID implements satellite.APIKeys ordered by name -func (keys *apikeys) GetByProjectID(ctx context.Context, projectID uuid.UUID) ([]satellite.APIKey, error) { +func (keys *apikeys) GetByProjectID(ctx context.Context, projectID uuid.UUID) ([]satellite.APIKeyInfo, error) { dbKeys, err := keys.db.All_ApiKey_By_ProjectId_OrderBy_Asc_Name(ctx, dbx.ApiKey_ProjectId(projectID[:])) if err != nil { return nil, err } - var apiKeys []satellite.APIKey + var apiKeys []satellite.APIKeyInfo var parseErr errs.Group for _, key := range dbKeys { - apiKey, err := toAPIKey(key) + info, err := fromDBXAPIKey(key) if err != nil { parseErr.Add(err) continue } - apiKeys = append(apiKeys, *apiKey) + apiKeys = append(apiKeys, *info) } if err := parseErr.Err(); err != nil { @@ -46,17 +46,17 @@ func (keys *apikeys) GetByProjectID(ctx context.Context, projectID uuid.UUID) ([ } // Get implements satellite.APIKeys -func (keys *apikeys) Get(ctx context.Context, id uuid.UUID) (*satellite.APIKey, error) { +func (keys *apikeys) Get(ctx context.Context, id uuid.UUID) (*satellite.APIKeyInfo, error) { dbKey, err := keys.db.Get_ApiKey_By_Id(ctx, dbx.ApiKey_Id(id[:])) if err != nil { return nil, err } - return toAPIKey(dbKey) + return fromDBXAPIKey(dbKey) } // Create implements satellite.APIKeys -func (keys *apikeys) Create(ctx context.Context, key satellite.APIKey) (*satellite.APIKey, error) { +func (keys *apikeys) Create(ctx context.Context, key satellite.APIKey, info satellite.APIKeyInfo) (*satellite.APIKeyInfo, error) { id, err := uuid.New() if err != nil { return nil, err @@ -65,20 +65,20 @@ func (keys *apikeys) Create(ctx context.Context, key satellite.APIKey) (*satelli dbKey, err := keys.db.Create_ApiKey( ctx, dbx.ApiKey_Id(id[:]), - dbx.ApiKey_ProjectId(key.ProjectID[:]), - dbx.ApiKey_Key(key.Key), - dbx.ApiKey_Name(key.Name), + dbx.ApiKey_ProjectId(info.ProjectID[:]), + dbx.ApiKey_Key(key[:]), + dbx.ApiKey_Name(info.Name), ) if err != nil { return nil, err } - return toAPIKey(dbKey) + return fromDBXAPIKey(dbKey) } // Update implements satellite.APIKeys -func (keys *apikeys) Update(ctx context.Context, key satellite.APIKey) error { +func (keys *apikeys) Update(ctx context.Context, key satellite.APIKeyInfo) error { _, err := keys.db.Update_ApiKey_By_Id( ctx, dbx.ApiKey_Id(key.ID[:]), @@ -96,8 +96,8 @@ func (keys *apikeys) Delete(ctx context.Context, id uuid.UUID) error { return err } -// toAPIKey converts dbx.ApiKey to satellite.APIKey -func toAPIKey(key *dbx.ApiKey) (*satellite.APIKey, error) { +// fromDBXAPIKey converts dbx.ApiKey to satellite.APIKeyInfo +func fromDBXAPIKey(key *dbx.ApiKey) (*satellite.APIKeyInfo, error) { id, err := bytesToUUID(key.Id) if err != nil { return nil, err @@ -108,11 +108,10 @@ func toAPIKey(key *dbx.ApiKey) (*satellite.APIKey, error) { return nil, err } - return &satellite.APIKey{ + return &satellite.APIKeyInfo{ ID: id, ProjectID: projectID, Name: key.Name, - Key: key.Key, CreatedAt: key.CreatedAt, }, nil } diff --git a/pkg/satellite/satellitedb/dbx/satellitedb.dbx b/pkg/satellite/satellitedb/dbx/satellitedb.dbx index 04d06bc3f..8a319a39c 100644 --- a/pkg/satellite/satellitedb/dbx/satellitedb.dbx +++ b/pkg/satellite/satellitedb/dbx/satellitedb.dbx @@ -62,10 +62,6 @@ model project_member ( ) read all ( - select project_member - where project_member.project_id = ? -) -read one ( select project_member where project_member.member_id = ? ) diff --git a/pkg/satellite/satellitedb/dbx/satellitedb.dbx.go b/pkg/satellite/satellitedb/dbx/satellitedb.dbx.go index cebc0a799..ae492ddfc 100644 --- a/pkg/satellite/satellitedb/dbx/satellitedb.dbx.go +++ b/pkg/satellite/satellitedb/dbx/satellitedb.dbx.go @@ -1200,14 +1200,14 @@ func (obj *sqlite3Impl) All_Project_By_ProjectMember_MemberId(ctx context.Contex } -func (obj *sqlite3Impl) All_ProjectMember_By_ProjectId(ctx context.Context, - project_member_project_id ProjectMember_ProjectId_Field) ( +func (obj *sqlite3Impl) All_ProjectMember_By_MemberId(ctx context.Context, + project_member_member_id ProjectMember_MemberId_Field) ( rows []*ProjectMember, err error) { - var __embed_stmt = __sqlbundle_Literal("SELECT project_members.member_id, project_members.project_id, project_members.created_at FROM project_members WHERE project_members.project_id = ?") + var __embed_stmt = __sqlbundle_Literal("SELECT project_members.member_id, project_members.project_id, project_members.created_at FROM project_members WHERE project_members.member_id = ?") var __values []interface{} - __values = append(__values, project_member_project_id.value()) + __values = append(__values, project_member_member_id.value()) var __stmt = __sqlbundle_Render(obj.dialect, __embed_stmt) obj.logStmt(__stmt, __values...) @@ -1233,49 +1233,6 @@ func (obj *sqlite3Impl) All_ProjectMember_By_ProjectId(ctx context.Context, } -func (obj *sqlite3Impl) Get_ProjectMember_By_MemberId(ctx context.Context, - project_member_member_id ProjectMember_MemberId_Field) ( - project_member *ProjectMember, err error) { - - var __embed_stmt = __sqlbundle_Literal("SELECT project_members.member_id, project_members.project_id, project_members.created_at FROM project_members WHERE project_members.member_id = ? LIMIT 2") - - var __values []interface{} - __values = append(__values, project_member_member_id.value()) - - var __stmt = __sqlbundle_Render(obj.dialect, __embed_stmt) - obj.logStmt(__stmt, __values...) - - __rows, err := obj.driver.Query(__stmt, __values...) - if err != nil { - return nil, obj.makeErr(err) - } - defer __rows.Close() - - if !__rows.Next() { - if err := __rows.Err(); err != nil { - return nil, obj.makeErr(err) - } - return nil, makeErr(sql.ErrNoRows) - } - - project_member = &ProjectMember{} - err = __rows.Scan(&project_member.MemberId, &project_member.ProjectId, &project_member.CreatedAt) - if err != nil { - return nil, obj.makeErr(err) - } - - if __rows.Next() { - return nil, tooManyRows("ProjectMember_By_MemberId") - } - - if err := __rows.Err(); err != nil { - return nil, obj.makeErr(err) - } - - return project_member, nil - -} - func (obj *sqlite3Impl) Limited_ProjectMember_By_ProjectId(ctx context.Context, project_member_project_id ProjectMember_ProjectId_Field, limit int, offset int64) ( @@ -1837,14 +1794,14 @@ func (rx *Rx) All_Project(ctx context.Context) ( return tx.All_Project(ctx) } -func (rx *Rx) All_ProjectMember_By_ProjectId(ctx context.Context, - project_member_project_id ProjectMember_ProjectId_Field) ( +func (rx *Rx) All_ProjectMember_By_MemberId(ctx context.Context, + project_member_member_id ProjectMember_MemberId_Field) ( rows []*ProjectMember, err error) { var tx *Tx if tx, err = rx.getTx(ctx); err != nil { return } - return tx.All_ProjectMember_By_ProjectId(ctx, project_member_project_id) + return tx.All_ProjectMember_By_MemberId(ctx, project_member_member_id) } func (rx *Rx) All_Project_By_ProjectMember_MemberId(ctx context.Context, @@ -1963,16 +1920,6 @@ func (rx *Rx) Get_ApiKey_By_Id(ctx context.Context, return tx.Get_ApiKey_By_Id(ctx, api_key_id) } -func (rx *Rx) Get_ProjectMember_By_MemberId(ctx context.Context, - project_member_member_id ProjectMember_MemberId_Field) ( - project_member *ProjectMember, err error) { - var tx *Tx - if tx, err = rx.getTx(ctx); err != nil { - return - } - return tx.Get_ProjectMember_By_MemberId(ctx, project_member_member_id) -} - func (rx *Rx) Get_Project_By_Id(ctx context.Context, project_id Project_Id_Field) ( project *Project, err error) { @@ -2055,8 +2002,8 @@ type Methods interface { All_Project(ctx context.Context) ( rows []*Project, err error) - All_ProjectMember_By_ProjectId(ctx context.Context, - project_member_project_id ProjectMember_ProjectId_Field) ( + All_ProjectMember_By_MemberId(ctx context.Context, + project_member_member_id ProjectMember_MemberId_Field) ( rows []*ProjectMember, err error) All_Project_By_ProjectMember_MemberId(ctx context.Context, @@ -2111,10 +2058,6 @@ type Methods interface { api_key_id ApiKey_Id_Field) ( api_key *ApiKey, err error) - Get_ProjectMember_By_MemberId(ctx context.Context, - project_member_member_id ProjectMember_MemberId_Field) ( - project_member *ProjectMember, err error) - Get_Project_By_Id(ctx context.Context, project_id Project_Id_Field) ( project *Project, err error) diff --git a/pkg/satellite/satellitedb/projectmembers.go b/pkg/satellite/satellitedb/projectmembers.go index 2e9dca7d5..889f84dfb 100644 --- a/pkg/satellite/satellitedb/projectmembers.go +++ b/pkg/satellite/satellitedb/projectmembers.go @@ -20,13 +20,13 @@ type projectMembers struct { } // GetByMemberID is a method for querying project member from the database by memberID. -func (pm *projectMembers) GetByMemberID(ctx context.Context, memberID uuid.UUID) (*satellite.ProjectMember, error) { - projectMemberDbx, err := pm.db.Get_ProjectMember_By_MemberId(ctx, dbx.ProjectMember_MemberId(memberID[:])) +func (pm *projectMembers) GetByMemberID(ctx context.Context, memberID uuid.UUID) ([]satellite.ProjectMember, error) { + projectMembersDbx, err := pm.db.All_ProjectMember_By_MemberId(ctx, dbx.ProjectMember_MemberId(memberID[:])) if err != nil { return nil, err } - return projectMemberFromDBX(projectMemberDbx) + return projectMembersFromDbxSlice(projectMembersDbx) } // GetByProjectID is a method for querying project members from the database by projectID, offset and limit. diff --git a/pkg/satellite/satellitedb/projectmembers_test.go b/pkg/satellite/satellitedb/projectmembers_test.go index 22d22e485..b56ba5eb4 100644 --- a/pkg/satellite/satellitedb/projectmembers_test.go +++ b/pkg/satellite/satellitedb/projectmembers_test.go @@ -97,20 +97,20 @@ func TestProjectMembersRepository(t *testing.T) { t.Run("Get member by memberID success", func(t *testing.T) { originalMember1 := createdUsers[0] - selectedMember1, err := projectMembers.GetByMemberID(ctx, originalMember1.ID) + selectedMembers1, err := projectMembers.GetByMemberID(ctx, originalMember1.ID) - assert.NotNil(t, selectedMember1) + assert.NotNil(t, selectedMembers1) assert.Nil(t, err) assert.NoError(t, err) - assert.Equal(t, originalMember1.ID, selectedMember1.MemberID) + assert.Equal(t, originalMember1.ID, selectedMembers1[0].MemberID) originalMember2 := createdUsers[1] - selectedMember2, err := projectMembers.GetByMemberID(ctx, originalMember2.ID) + selectedMembers2, err := projectMembers.GetByMemberID(ctx, originalMember2.ID) - assert.NotNil(t, selectedMember2) + assert.NotNil(t, selectedMembers2) assert.Nil(t, err) assert.NoError(t, err) - assert.Equal(t, originalMember2.ID, selectedMember2.MemberID) + assert.Equal(t, originalMember2.ID, selectedMembers2[0].MemberID) }) } diff --git a/pkg/satellite/satelliteweb/satelliteql/apikey.go b/pkg/satellite/satelliteweb/satelliteql/apikey.go new file mode 100644 index 000000000..1e9f443f2 --- /dev/null +++ b/pkg/satellite/satelliteweb/satelliteql/apikey.go @@ -0,0 +1,59 @@ +// Copyright (C) 2018 Storj Labs, Inc. +// See LICENSE for copying information. + +package satelliteql + +import ( + "github.com/graphql-go/graphql" + + "storj.io/storj/pkg/satellite" +) + +const ( + apiKeyInfoType = "keyInfo" + createAPIKeyType = "graphqlCreateAPIKey" + + fieldKey = "key" +) + +// graphqlAPIKeyInfo creates satellite.APIKeyInfo graphql object +func graphqlAPIKeyInfo() *graphql.Object { + return graphql.NewObject(graphql.ObjectConfig{ + Name: apiKeyInfoType, + Fields: graphql.Fields{ + fieldID: &graphql.Field{ + Type: graphql.String, + }, + fieldProjectID: &graphql.Field{ + Type: graphql.String, + }, + fieldName: &graphql.Field{ + Type: graphql.String, + }, + fieldCreatedAt: &graphql.Field{ + Type: graphql.DateTime, + }, + }, + }) +} + +// graphqlCreateAPIKey creates createAPIKey graphql object +func graphqlCreateAPIKey(types Types) *graphql.Object { + return graphql.NewObject(graphql.ObjectConfig{ + Name: createAPIKeyType, + Fields: graphql.Fields{ + fieldKey: &graphql.Field{ + Type: graphql.String, + }, + apiKeyInfoType: &graphql.Field{ + Type: types.APIKeyInfo(), + }, + }, + }) +} + +// createAPIKey holds satellite.APIKey and satellite.APIKeyInfo +type createAPIKey struct { + Key *satellite.APIKey + KeyInfo *satellite.APIKeyInfo +} diff --git a/pkg/satellite/satelliteweb/satelliteql/mutation.go b/pkg/satellite/satelliteweb/satelliteql/mutation.go index ff1a5a67b..b517774ca 100644 --- a/pkg/satellite/satelliteweb/satelliteql/mutation.go +++ b/pkg/satellite/satelliteweb/satelliteql/mutation.go @@ -23,8 +23,11 @@ const ( deleteProjectMutation = "deleteProject" updateProjectDescriptionMutation = "updateProjectDescription" - addProjectMemberMutation = "addProjectMembers" - deleteProjectMemberMutation = "deleteProjectMembers" + addProjectMembersMutation = "addProjectMembers" + deleteProjectMembersMutation = "deleteProjectMembers" + + createAPIKeyMutation = "createAPIKey" + deleteAPIKeyMutation = "deleteAPIKey" input = "input" @@ -189,7 +192,7 @@ func rootMutation(service *satellite.Service, types Types) *graphql.Object { }, }, // add user as member of given project - addProjectMemberMutation: &graphql.Field{ + addProjectMembersMutation: &graphql.Field{ Type: types.Project(), Args: graphql.FieldConfigArgument{ fieldProjectID: &graphql.ArgumentConfig{ @@ -222,7 +225,7 @@ func rootMutation(service *satellite.Service, types Types) *graphql.Object { }, }, // delete user membership for given project - deleteProjectMemberMutation: &graphql.Field{ + deleteProjectMembersMutation: &graphql.Field{ Type: types.Project(), Args: graphql.FieldConfigArgument{ fieldProjectID: &graphql.ArgumentConfig{ @@ -254,6 +257,66 @@ func rootMutation(service *satellite.Service, types Types) *graphql.Object { return service.GetProject(p.Context, *projectID) }, }, + // creates new api key + createAPIKeyMutation: &graphql.Field{ + Type: types.CreateAPIKey(), + Args: graphql.FieldConfigArgument{ + fieldProjectID: &graphql.ArgumentConfig{ + Type: graphql.NewNonNull(graphql.String), + }, + fieldName: &graphql.ArgumentConfig{ + Type: graphql.NewNonNull(graphql.String), + }, + }, + Resolve: func(p graphql.ResolveParams) (interface{}, error) { + projectID, _ := p.Args[fieldProjectID].(string) + name, _ := p.Args[fieldName].(string) + + pID, err := uuid.Parse(projectID) + if err != nil { + return nil, err + } + + info, key, err := service.CreateAPIKey(p.Context, *pID, name) + if err != nil { + return nil, err + } + + return createAPIKey{ + Key: key, + KeyInfo: info, + }, nil + }, + }, + // deletes api key + deleteAPIKeyMutation: &graphql.Field{ + Type: types.APIKeyInfo(), + Args: graphql.FieldConfigArgument{ + fieldID: &graphql.ArgumentConfig{ + Type: graphql.NewNonNull(graphql.String), + }, + }, + Resolve: func(p graphql.ResolveParams) (interface{}, error) { + keyID, _ := p.Args[fieldID].(string) + + id, err := uuid.Parse(keyID) + if err != nil { + return nil, err + } + + key, err := service.GetAPIKeyInfo(p.Context, *id) + if err != nil { + return nil, err + } + + err = service.DeleteAPIKey(p.Context, *id) + if err != nil { + return nil, err + } + + return key, nil + }, + }, }, }) } diff --git a/pkg/satellite/satelliteweb/satelliteql/project.go b/pkg/satellite/satelliteweb/satelliteql/project.go index 9dcd7de64..df278bead 100644 --- a/pkg/satellite/satelliteweb/satelliteql/project.go +++ b/pkg/satellite/satelliteweb/satelliteql/project.go @@ -18,6 +18,7 @@ const ( // Used in input model fieldIsTermsAccepted = "isTermsAccepted" fieldMembers = "members" + fieldAPIKeys = "apiKeys" limit = "limit" offset = "offset" @@ -80,6 +81,14 @@ func graphqlProject(service *satellite.Service, types Types) *graphql.Object { return users, nil }, }, + fieldAPIKeys: &graphql.Field{ + Type: graphql.NewList(types.APIKeyInfo()), + Resolve: func(p graphql.ResolveParams) (interface{}, error) { + project, _ := p.Source.(*satellite.Project) + + return service.GetAPIKeysInfoByProjectID(p.Context, project.ID) + }, + }, }, }) } diff --git a/pkg/satellite/satelliteweb/satelliteql/typecreator.go b/pkg/satellite/satelliteweb/satelliteql/typecreator.go index 21425e260..aa01c571f 100644 --- a/pkg/satellite/satelliteweb/satelliteql/typecreator.go +++ b/pkg/satellite/satelliteweb/satelliteql/typecreator.go @@ -19,6 +19,8 @@ type Types interface { User() *graphql.Object Project() *graphql.Object ProjectMember() *graphql.Object + APIKeyInfo() *graphql.Object + CreateAPIKey() *graphql.Object UserInput() *graphql.InputObject ProjectInput() *graphql.InputObject @@ -34,6 +36,8 @@ type TypeCreator struct { user *graphql.Object project *graphql.Object projectMember *graphql.Object + apiKeyInfo *graphql.Object + createAPIKey *graphql.Object userInput *graphql.InputObject projectInput *graphql.InputObject @@ -58,6 +62,16 @@ func (c *TypeCreator) Create(service *satellite.Service) error { return err } + c.apiKeyInfo = graphqlAPIKeyInfo() + if err := c.apiKeyInfo.Error(); err != nil { + return err + } + + c.createAPIKey = graphqlCreateAPIKey(c) + if err := c.createAPIKey.Error(); err != nil { + return err + } + c.projectMember = graphqlProjectMember(service, c) if err := c.projectMember.Error(); err != nil { return err @@ -107,6 +121,17 @@ func (c *TypeCreator) User() *graphql.Object { return c.user } +// APIKeyInfo returns instance of satellite.APIKeyInfo *graphql.Object +func (c *TypeCreator) APIKeyInfo() *graphql.Object { + return c.apiKeyInfo +} + +// CreateAPIKey encapsulates api key and key info +// returns *graphql.Object +func (c *TypeCreator) CreateAPIKey() *graphql.Object { + return c.createAPIKey +} + // Project returns instance of satellite.Project *graphql.Object func (c *TypeCreator) Project() *graphql.Object { return c.project diff --git a/pkg/satellite/service.go b/pkg/satellite/service.go index c48198bb1..12c1c737d 100644 --- a/pkg/satellite/service.go +++ b/pkg/satellite/service.go @@ -5,9 +5,7 @@ package satellite import ( "context" - "crypto/rand" "crypto/subtle" - "io" "time" "golang.org/x/crypto/bcrypt" @@ -285,11 +283,15 @@ func (s *Service) UpdateProject(ctx context.Context, projectID uuid.UUID, descri // AddProjectMembers adds users by email to given project func (s *Service) AddProjectMembers(ctx context.Context, projectID uuid.UUID, emails []string) (err error) { defer mon.Task()(&ctx)(&err) - _, err = GetAuth(ctx) + auth, err := GetAuth(ctx) if err != nil { return err } + if _, err = s.isProjectMember(ctx, auth.User.ID, projectID); err != nil { + return ErrUnauthorized.Wrap(err) + } + var userIDs []uuid.UUID var userErr errs.Group @@ -305,7 +307,7 @@ func (s *Service) AddProjectMembers(ctx context.Context, projectID uuid.UUID, em userIDs = append(userIDs, user.ID) } - if err := userErr.Err(); err != nil { + if err = userErr.Err(); err != nil { return err } @@ -315,25 +317,38 @@ func (s *Service) AddProjectMembers(ctx context.Context, projectID uuid.UUID, em return err } + defer func() { + if err != nil { + err = errs.Combine(err, tx.Rollback()) + return + } + + err = tx.Commit() + }() + for _, uID := range userIDs { - _, err := tx.ProjectMembers().Insert(ctx, uID, projectID) + _, err = tx.ProjectMembers().Insert(ctx, uID, projectID) if err != nil { - return errs.Combine(err, tx.Rollback()) + return err } } - return tx.Commit() + return nil } // DeleteProjectMembers removes users by email from given project func (s *Service) DeleteProjectMembers(ctx context.Context, projectID uuid.UUID, emails []string) (err error) { defer mon.Task()(&ctx)(&err) - _, err = GetAuth(ctx) + auth, err := GetAuth(ctx) if err != nil { return err } + if _, err = s.isProjectMember(ctx, auth.User.ID, projectID); err != nil { + return ErrUnauthorized.Wrap(err) + } + var userIDs []uuid.UUID var userErr errs.Group @@ -349,7 +364,7 @@ func (s *Service) DeleteProjectMembers(ctx context.Context, projectID uuid.UUID, userIDs = append(userIDs, user.ID) } - if err := userErr.Err(); err != nil { + if err = userErr.Err(); err != nil { return err } @@ -359,15 +374,24 @@ func (s *Service) DeleteProjectMembers(ctx context.Context, projectID uuid.UUID, return err } + defer func() { + if err != nil { + err = errs.Combine(err, tx.Rollback()) + return + } + + err = tx.Commit() + }() + for _, uID := range userIDs { - err := tx.ProjectMembers().Delete(ctx, uID, projectID) + err = tx.ProjectMembers().Delete(ctx, uID, projectID) if err != nil { - return errs.Combine(err, tx.Rollback()) + return err } } - return tx.Commit() + return nil } // GetProjectMembers returns ProjectMembers for given Project @@ -389,37 +413,91 @@ func (s *Service) GetProjectMembers(ctx context.Context, projectID uuid.UUID, li return s.store.ProjectMembers().GetByProjectID(ctx, projectID, limit, offset) } -// apiKey is a mock api key type -type apiKey [24]byte +// CreateAPIKey creates new api key +func (s *Service) CreateAPIKey(ctx context.Context, projectID uuid.UUID, name string) (*APIKeyInfo, *APIKey, error) { + var err error + defer mon.Task()(&ctx)(&err) -// createAPIKey creates new mock api key -func createAPIKey() (*apiKey, error) { - key := new(apiKey) - n, err := io.ReadFull(rand.Reader, key[:]) - if err != nil || n != 24 { - return nil, errs.New("error creating apikey") + auth, err := GetAuth(ctx) + if err != nil { + return nil, nil, err + } + + _, err = s.isProjectMember(ctx, auth.User.ID, projectID) + if err != nil { + return nil, nil, ErrUnauthorized.Wrap(err) + } + + key, err := createAPIKey() + if err != nil { + return nil, nil, err + } + + info, err := s.store.APIKeys().Create(ctx, *key, APIKeyInfo{ + Name: name, + ProjectID: projectID, + }) + return info, key, err +} + +// GetAPIKeyInfo retrieves api key by id +func (s *Service) GetAPIKeyInfo(ctx context.Context, id uuid.UUID) (*APIKeyInfo, error) { + var err error + defer mon.Task()(&ctx)(&err) + + auth, err := GetAuth(ctx) + if err != nil { + return nil, err + } + + key, err := s.store.APIKeys().Get(ctx, id) + if err != nil { + return nil, err + } + + _, err = s.isProjectMember(ctx, auth.User.ID, key.ProjectID) + if err != nil { + return nil, ErrUnauthorized.Wrap(err) } return key, nil } -// CreateAPIKey creates new api key -func (s *Service) CreateAPIKey(ctx context.Context, projectID uuid.UUID, name string) (*APIKey, error) { - _, err := GetAuth(ctx) +// DeleteAPIKey deletes api key by id +func (s *Service) DeleteAPIKey(ctx context.Context, id uuid.UUID) (err error) { + defer mon.Task()(&ctx)(&err) + auth, err := GetAuth(ctx) + if err != nil { + return err + } + + key, err := s.store.APIKeys().Get(ctx, id) + if err != nil { + return err + } + + _, err = s.isProjectMember(ctx, auth.User.ID, key.ProjectID) + if err != nil { + return ErrUnauthorized.Wrap(err) + } + + return s.store.APIKeys().Delete(ctx, id) +} + +// GetAPIKeysInfoByProjectID retrieves all api keys for a given project +func (s *Service) GetAPIKeysInfoByProjectID(ctx context.Context, projectID uuid.UUID) (info []APIKeyInfo, err error) { + defer mon.Task()(&ctx)(&err) + auth, err := GetAuth(ctx) if err != nil { return nil, err } - key, err := createAPIKey() + _, err = s.isProjectMember(ctx, auth.User.ID, projectID) if err != nil { - return nil, err + return nil, ErrUnauthorized.Wrap(err) } - return s.store.APIKeys().Create(ctx, APIKey{ - Name: name, - Key: key[:], - ProjectID: projectID, - }) + return s.store.APIKeys().GetByProjectID(ctx, projectID) } // Authorize validates token from context and returns authorized Authorization @@ -501,3 +579,35 @@ func (s *Service) authorize(ctx context.Context, claims *satelliteauth.Claims) ( return user, nil } + +// isProjectMember is return type of isProjectMember service method +type isProjectMember struct { + project *Project + membership *ProjectMember +} + +// ErrNoMembership is error type of not belonging to a specific project +var ErrNoMembership = errs.Class("no membership error") + +// isProjectMember checks if the user is a member of given project +func (s *Service) isProjectMember(ctx context.Context, userID uuid.UUID, projectID uuid.UUID) (result isProjectMember, err error) { + project, err := s.store.Projects().Get(ctx, projectID) + if err != nil { + return + } + + memberships, err := s.store.ProjectMembers().GetByMemberID(ctx, userID) + if err != nil { + return + } + + for _, membership := range memberships { + if membership.ProjectID == projectID { + result.membership = &membership + result.project = project + return + } + } + + return isProjectMember{}, ErrNoMembership.New("user % is not a member of project %s", userID, project.ID) +}