Skip to content

Commit

Permalink
feat: add support for user to join a whitelisted org domain
Browse files Browse the repository at this point in the history
  • Loading branch information
Chief-Rishab committed Aug 9, 2023
1 parent 81b370e commit fc7cc7c
Show file tree
Hide file tree
Showing 14 changed files with 3,031 additions and 2,968 deletions.
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ TAG := $(shell git rev-list --tags --max-count=1)
VERSION := $(shell git describe --tags ${TAG})
.PHONY: build check fmt lint test test-race vet test-cover-html help install proto ui
.DEFAULT_GOAL := build
PROTON_COMMIT := "225398a88d273a621cbe3c5ef3b92fe62f1cc7a1"
PROTON_COMMIT := "10e4be7df8fd679eb32c3e52b815c55259a4c99b"

ui:
@echo " > generating ui build"
Expand Down
5 changes: 5 additions & 0 deletions cmd/serve.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import (
"time"

"github.com/raystack/frontier/core/audit"
"github.com/raystack/frontier/core/domain"

"github.com/raystack/frontier/core/serviceuser"

Expand Down Expand Up @@ -254,6 +255,9 @@ func buildAPIDependencies(
organizationRepository := postgres.NewOrganizationRepository(dbc)
organizationService := organization.NewService(organizationRepository, relationService, userService, authnService)

domainRepository := postgres.NewDomainRepository(logger, dbc)
domainService := domain.NewService(domainRepository, userService, organizationService)

projectRepository := postgres.NewProjectRepository(dbc)
projectService := project.NewService(projectRepository, relationService, userService)

Expand Down Expand Up @@ -308,6 +312,7 @@ func buildAPIDependencies(
InvitationService: invitationService,
ServiceUserService: serviceUserService,
AuditService: auditService,
DomainService: domainService,
}
return dependencies, nil
}
Expand Down
4 changes: 2 additions & 2 deletions core/domain/domain.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,9 @@ import (
type Repository interface {
Create(ctx context.Context, domain Domain) error
Get(ctx context.Context, id string) (Domain, error)
Update(ctx context.Context, id string, domain Domain) (Domain, error)
Update(ctx context.Context, domain Domain) (Domain, error)
List(ctx context.Context, flt Filter) ([]Domain, error)
Delete(ctx context.Context, id string) (string, error)
Delete(ctx context.Context, id string) error
}

type Domain struct {
Expand Down
4 changes: 3 additions & 1 deletion core/domain/errors.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,7 @@ package domain
import "errors"

var (
ErrNotExist = errors.New("org domain request does not exist")
ErrNotExist = errors.New("org domain request does not exist")
ErrDomainsMisMatch = errors.New("user domain does not match the organization domain")
ErrInvalidId = errors.New("invalid domain id")
)
85 changes: 80 additions & 5 deletions core/domain/service.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,20 +6,39 @@ import (
"encoding/base64"
"fmt"
"net"
"strings"

"github.com/raystack/frontier/core/authenticate"
"github.com/raystack/frontier/core/organization"
"github.com/raystack/frontier/core/user"
"github.com/raystack/frontier/internal/bootstrap/schema"
)

type UserService interface {
GetByID(ctx context.Context, id string) (user.User, error)
}

type OrgService interface {
ListByUser(ctx context.Context, userID string) ([]organization.Organization, error)
AddMember(ctx context.Context, orgID, relationName string, principal authenticate.Principal) error
}

type Service struct {
repository Repository
repository Repository
userService UserService
orgService OrgService
}

const (
DNSChallenge = "_frontier-challenge.%s"
txtLength = 16
)

func NewService(repository Repository) *Service {
func NewService(repository Repository, userService UserService, orgService OrgService) *Service {
return &Service{
repository: repository,
repository: repository,
userService: userService,
orgService: orgService,
}
}

Expand All @@ -34,7 +53,7 @@ func (s Service) List(ctx context.Context, flt Filter) ([]Domain, error) {
}

// Remove an organization's whitelisted domain from the database
func (s Service) Delete(ctx context.Context, id string) (string, error) {
func (s Service) Delete(ctx context.Context, id string) error {
return s.repository.Delete(ctx, id)
}

Expand Down Expand Up @@ -68,13 +87,61 @@ func (s Service) VerifyDomain(ctx context.Context, id string) error {
for _, txtRecord := range txtRecords {
if txtRecord == domain.Token {
domain.Verified = true
s.repository.Update(ctx, id, domain)
s.repository.Update(ctx, domain)
}
}

return ErrNotExist
}

// Join an organization as a member if the user domain matches the org whitelisted domains
func (s Service) Join(ctx context.Context, orgID string, userId string) error {
currUser, err := s.userService.GetByID(ctx, userId)
if err != nil {
return err
}

// check if user is already a member of the organization. if yes, do nothing and return nil
userOrgs, err := s.orgService.ListByUser(ctx, currUser.ID)
if err != nil {
return err
}

for _, org := range userOrgs {
if org.ID == orgID {
return nil
}
}

userDomain := extractDomainFromEmail(currUser.Email)
if userDomain == "" {
return user.ErrInvalidEmail
}

// check if user domain matches the org whitelisted domains
orgTrustedDomains, err := s.List(ctx, Filter{
OrgID: orgID,
Verified: true,
})
if err != nil {
return err
}

for _, dmn := range orgTrustedDomains {
if userDomain == dmn.Name {
if err = s.orgService.AddMember(ctx, orgID, schema.MemberRelationName, authenticate.Principal{
ID: currUser.ID,
Type: schema.UserPrincipal,
}); err != nil {
return err
}
return nil
}
}

return ErrDomainsMisMatch
}

func generateRandomTXT() (string, error) {
randomBytes := make([]byte, txtLength)
_, err := rand.Read(randomBytes)
Expand All @@ -86,3 +153,11 @@ func generateRandomTXT() (string, error) {
txtRecord := base64.StdEncoding.EncodeToString(randomBytes)
return txtRecord, nil
}

func extractDomainFromEmail(email string) string {
parts := strings.Split(email, "@")
if len(parts) == 2 {
return parts[1]
}
return ""
}
11 changes: 5 additions & 6 deletions core/organization/errors.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,9 @@ package organization
import "errors"

var (
ErrNotExist = errors.New("org doesn't exist")
ErrInvalidUUID = errors.New("invalid syntax of uuid")
ErrInvalidID = errors.New("org id is invalid")
ErrConflict = errors.New("org already exist")
ErrInvalidDetail = errors.New("invalid org detail")
ErrDomainsNotMatch = errors.New("user domain does not match the organization domain")
ErrNotExist = errors.New("org doesn't exist")
ErrInvalidUUID = errors.New("invalid syntax of uuid")
ErrInvalidID = errors.New("org id is invalid")
ErrConflict = errors.New("org already exist")
ErrInvalidDetail = errors.New("invalid org detail")
)
31 changes: 6 additions & 25 deletions docs/docs/apis/frontier-service-join-organization.api.mdx
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
---
id: frontier-service-join-organization
title: "Join organization"
description: "Allows a user to join the Org if one is not a part of it. The user will only be able to join when the user email's domain matches the organization's whitelisted domains."
description: "Allows the current logged in user to join the Org if one is not a part of it. The user will only be able to join when the user email's domain matches the organization's whitelisted domains."
sidebar_label: "Join organization"
hide_title: true
hide_table_of_contents: true
api:
{
"description": "Allows a user to join the Org if one is not a part of it. The user will only be able to join when the user email's domain matches the organization's whitelisted domains.",
"description": "Allows the current logged in user to join the Org if one is not a part of it. The user will only be able to join when the user email's domain matches the organization's whitelisted domains.",
"operationId": "FrontierService_JoinOrganization",
"responses":
{
Expand Down Expand Up @@ -206,13 +206,6 @@ api:
"required": true,
"schema": { "type": "string" },
},
{
"name": "userId",
"description": "user_id is email id of user who wants to join the organization.",
"in": "query",
"required": false,
"schema": { "type": "string" },
},
],
"tags": ["Organization"],
"method": "post",
Expand Down Expand Up @@ -259,26 +252,14 @@ api:
"name": "Join organization",
"description":
{
"content": "Allows a user to join the Org if one is not a part of it. The user will only be able to join when the user email's domain matches the organization's whitelisted domains.",
"content": "Allows the current logged in user to join the Org if one is not a part of it. The user will only be able to join when the user email's domain matches the organization's whitelisted domains.",
"type": "text/plain",
},
"url":
{
"path": ["v1beta1", "organizations", ":orgId", "join"],
"host": ["{{baseUrl}}"],
"query":
[
{
"disabled": false,
"description":
{
"content": "user_id is email id of user who wants to join the organization.",
"type": "text/plain",
},
"key": "userId",
"value": "",
},
],
"query": [],
"variable":
[
{
Expand Down Expand Up @@ -311,9 +292,9 @@ import TabItem from "@theme/TabItem";

## Join organization

Allows a user to join the Org if one is not a part of it. The user will only be able to join when the user email's domain matches the organization's whitelisted domains.
Allows the current logged in user to join the Org if one is not a part of it. The user will only be able to join when the user email's domain matches the organization's whitelisted domains.

<details style={{"marginBottom":"1rem"}} data-collapsed={false} open={true}><summary style={{}}><strong>Path Parameters</strong></summary><div><ul><ParamsItem className={"paramsItem"} param={{"name":"orgId","in":"path","required":true,"schema":{"type":"string"}}}></ParamsItem></ul></div></details><details style={{"marginBottom":"1rem"}} data-collapsed={false} open={true}><summary style={{}}><strong>Query Parameters</strong></summary><div><ul><ParamsItem className={"paramsItem"} param={{"name":"userId","description":"user_id is email id of user who wants to join the organization.","in":"query","required":false,"schema":{"type":"string"}}}></ParamsItem></ul></div></details><div><ApiTabs><TabItem label={"200"} value={"200"}><div>
<details style={{"marginBottom":"1rem"}} data-collapsed={false} open={true}><summary style={{}}><strong>Path Parameters</strong></summary><div><ul><ParamsItem className={"paramsItem"} param={{"name":"orgId","in":"path","required":true,"schema":{"type":"string"}}}></ParamsItem></ul></div></details><div><ApiTabs><TabItem label={"200"} value={"200"}><div>

A successful response.

Expand Down
2 changes: 2 additions & 0 deletions internal/api/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"github.com/raystack/frontier/core/authenticate"
"github.com/raystack/frontier/core/authenticate/session"
"github.com/raystack/frontier/core/deleter"
"github.com/raystack/frontier/core/domain"
"github.com/raystack/frontier/core/group"
"github.com/raystack/frontier/core/invitation"
"github.com/raystack/frontier/core/metaschema"
Expand Down Expand Up @@ -44,4 +45,5 @@ type Deps struct {
InvitationService *invitation.Service
ServiceUserService *serviceuser.Service
AuditService *audit.Service
DomainService *domain.Service
}
32 changes: 32 additions & 0 deletions internal/api/v1beta1/domain.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (

grpczap "github.com/grpc-ecosystem/go-grpc-middleware/logging/zap/ctxzap"
"github.com/raystack/frontier/core/domain"
"github.com/raystack/frontier/core/organization"
frontierv1beta1 "github.com/raystack/frontier/proto/v1beta1"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
Expand All @@ -13,6 +14,7 @@ import (

var (
grpcDomainNotFoundErr = status.Errorf(codes.NotFound, "domain whitelist request doesn't exist")
grpcDomainMisMatchErr = status.Errorf(codes.InvalidArgument, "user and org's whitelisted domains doesn't match")
)

type DomainService interface {
Expand All @@ -21,6 +23,7 @@ type DomainService interface {
Delete(ctx context.Context, id string) error
Create(ctx context.Context, toCreate domain.Domain) (domain.Domain, error)
VerifyDomain(ctx context.Context, id string) (domain.Domain, error)
Join(ctx context.Context, orgID string, userID string) error
}

func (h Handler) AddOrganizationDomain(ctx context.Context, request *frontierv1beta1.AddOrganizationDomainRequest) (*frontierv1beta1.AddOrganizationDomainResponse, error) {
Expand Down Expand Up @@ -85,6 +88,35 @@ func (h Handler) GetOrganizationDomain(ctx context.Context, request *frontierv1b
return &frontierv1beta1.GetOrganizationDomainResponse{Domain: &domainPB}, nil
}

func (h Handler) JoinOrganization(ctx context.Context, request *frontierv1beta1.JoinOrganizationRequest) (*frontierv1beta1.JoinOrganizationResponse, error) {
logger := grpczap.Extract(ctx)
orgId := request.GetOrgId()
if orgId == "" {
return nil, grpcBadBodyError
}

// get current user
principal, err := h.GetLoggedInPrincipal(ctx)
if err != nil {
logger.Error(err.Error())
return nil, grpcInternalServerError
}

if err := h.domainService.Join(ctx, orgId, principal.ID); err != nil {
logger.Error(err.Error())
switch err {
case organization.ErrNotExist:
return nil, grpcOrgNotFoundErr
case domain.ErrDomainsMisMatch:
return nil, grpcDomainMisMatchErr
default:
return nil, grpcInternalServerError
}
}

return &frontierv1beta1.JoinOrganizationResponse{}, nil
}

func (h Handler) VerifyOrgDomain(ctx context.Context, request *frontierv1beta1.VerifyOrgDomainRequest) (*frontierv1beta1.VerifyOrgDomainResponse, error) {
logger := grpczap.Extract(ctx)

Expand Down
Loading

0 comments on commit fc7cc7c

Please sign in to comment.