diff --git a/components/usermgmt/env.example b/components/usermgmt/env.example new file mode 100644 index 0000000..a2e783a --- /dev/null +++ b/components/usermgmt/env.example @@ -0,0 +1 @@ +APP_HOST_HTTP="http://localhost:8000" \ No newline at end of file diff --git a/components/usermgmt/gen/openapi/proto/rpc/v3/idp.swagger.json b/components/usermgmt/gen/openapi/proto/rpc/v3/idp.swagger.json new file mode 100644 index 0000000..25b091b --- /dev/null +++ b/components/usermgmt/gen/openapi/proto/rpc/v3/idp.swagger.json @@ -0,0 +1,881 @@ +{ + "swagger": "2.0", + "info": { + "title": "Identity Provider (IdP) Service", + "version": "2.0", + "contact": { + "name": "Rafay Dev" + } + }, + "tags": [ + { + "name": "Idp" + } + ], + "schemes": [ + "https" + ], + "consumes": [ + "application/json", + "application/yaml" + ], + "produces": [ + "application/json", + "application/yaml" + ], + "paths": { + "/auth/v3/sso/idp": { + "get": { + "operationId": "Idp_ListIdps", + "responses": { + "200": { + "description": "A successful response.", + "schema": { + "$ref": "#/definitions/v3IdpList" + } + }, + "403": { + "description": "Returned when the user does not have permission to access the resource.", + "schema": {} + }, + "404": { + "description": "Returned when the resource does not exist.", + "schema": { + "type": "string", + "format": "string" + } + }, + "default": { + "description": "An unexpected error response.", + "schema": { + "$ref": "#/definitions/googlerpcStatus" + } + } + }, + "tags": [ + "Idp" + ] + }, + "post": { + "operationId": "Idp_CreateIdp", + "responses": { + "200": { + "description": "A successful response.", + "schema": { + "$ref": "#/definitions/v3Idp" + } + }, + "201": { + "description": "Returned when idp is created successfully.", + "schema": {} + }, + "403": { + "description": "Returned when the user does not have permission to access the resource.", + "schema": {} + }, + "404": { + "description": "Returned when the resource does not exist.", + "schema": { + "type": "string", + "format": "string" + } + }, + "default": { + "description": "An unexpected error response.", + "schema": { + "$ref": "#/definitions/googlerpcStatus" + } + } + }, + "parameters": [ + { + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/v3Idp" + } + } + ], + "tags": [ + "Idp" + ] + } + }, + "/auth/v3/sso/idp/{metadata.id}": { + "get": { + "operationId": "Idp_GetIdp", + "responses": { + "200": { + "description": "A successful response.", + "schema": { + "$ref": "#/definitions/v3Idp" + } + }, + "403": { + "description": "Returned when the user does not have permission to access the resource.", + "schema": {} + }, + "404": { + "description": "Returned when the resource does not exist.", + "schema": { + "type": "string", + "format": "string" + } + }, + "default": { + "description": "An unexpected error response.", + "schema": { + "$ref": "#/definitions/googlerpcStatus" + } + } + }, + "parameters": [ + { + "name": "metadata.id", + "in": "path", + "required": true, + "type": "string" + }, + { + "name": "apiVersion", + "description": "API Version. API Version of the idp resource", + "in": "query", + "required": false, + "type": "string", + "default": "usermgmt.k8smgmt.io/v3" + }, + { + "name": "kind", + "description": "Kind. Kind of the idp resource", + "in": "query", + "required": false, + "type": "string", + "default": "Idp" + }, + { + "name": "metadata.name", + "description": "Name. name of the resource", + "in": "query", + "required": false, + "type": "string" + }, + { + "name": "metadata.description", + "description": "Description. description of the resource", + "in": "query", + "required": false, + "type": "string" + }, + { + "name": "metadata.project", + "description": "Project. Project of the resource", + "in": "query", + "required": false, + "type": "string" + }, + { + "name": "metadata.organization", + "description": "Organization. Organization to which the resource belongs", + "in": "query", + "required": false, + "type": "string" + }, + { + "name": "metadata.partner", + "description": "Partner. Partner to which the resource belongs", + "in": "query", + "required": false, + "type": "string" + }, + { + "name": "metadata.modifiedAt", + "in": "query", + "required": false, + "type": "string", + "format": "date-time" + }, + { + "name": "spec.idpName", + "in": "query", + "required": false, + "type": "string" + }, + { + "name": "spec.domain", + "in": "query", + "required": false, + "type": "string" + }, + { + "name": "spec.acsUrl", + "in": "query", + "required": false, + "type": "string" + }, + { + "name": "spec.ssoUrl", + "in": "query", + "required": false, + "type": "string" + }, + { + "name": "spec.idpCert", + "in": "query", + "required": false, + "type": "string" + }, + { + "name": "spec.spCert", + "in": "query", + "required": false, + "type": "string" + }, + { + "name": "spec.metadataUrl", + "in": "query", + "required": false, + "type": "string" + }, + { + "name": "spec.metadataFilename", + "in": "query", + "required": false, + "type": "string" + }, + { + "name": "spec.saeEnabled", + "in": "query", + "required": false, + "type": "boolean" + }, + { + "name": "spec.groupAttributeName", + "in": "query", + "required": false, + "type": "string" + }, + { + "name": "spec.nameIdFormat", + "in": "query", + "required": false, + "type": "string" + }, + { + "name": "spec.consumerBinding", + "in": "query", + "required": false, + "type": "string" + }, + { + "name": "spec.spEntityId", + "in": "query", + "required": false, + "type": "string" + }, + { + "name": "status.conditionType", + "description": "Condition Type. type of the status condition", + "in": "query", + "required": false, + "type": "string" + }, + { + "name": "status.conditionStatus", + "description": "Condition Status. status of the condition", + "in": "query", + "required": false, + "type": "string", + "enum": [ + "StatusNotSet", + "StatusSubmitted", + "StatusOK", + "StatusFailed" + ], + "default": "StatusNotSet" + }, + { + "name": "status.lastUpdated", + "description": "Last Updated. when the condition status is last updated", + "in": "query", + "required": false, + "type": "string", + "format": "date-time" + }, + { + "name": "status.reason", + "description": "Reason. reason of the last condition status", + "in": "query", + "required": false, + "type": "string" + } + ], + "tags": [ + "Idp" + ] + }, + "delete": { + "operationId": "Idp_DeleteIdp", + "responses": { + "200": { + "description": "A successful response.", + "schema": { + "properties": {} + } + }, + "204": { + "description": "Returned when idp is deleted successfully.", + "schema": {} + }, + "403": { + "description": "Returned when the user does not have permission to access the resource.", + "schema": {} + }, + "404": { + "description": "Returned when the resource does not exist.", + "schema": { + "type": "string", + "format": "string" + } + }, + "default": { + "description": "An unexpected error response.", + "schema": { + "$ref": "#/definitions/googlerpcStatus" + } + } + }, + "parameters": [ + { + "name": "metadata.id", + "in": "path", + "required": true, + "type": "string" + }, + { + "name": "apiVersion", + "description": "API Version. API Version of the idp resource", + "in": "query", + "required": false, + "type": "string", + "default": "usermgmt.k8smgmt.io/v3" + }, + { + "name": "kind", + "description": "Kind. Kind of the idp resource", + "in": "query", + "required": false, + "type": "string", + "default": "Idp" + }, + { + "name": "metadata.name", + "description": "Name. name of the resource", + "in": "query", + "required": false, + "type": "string" + }, + { + "name": "metadata.description", + "description": "Description. description of the resource", + "in": "query", + "required": false, + "type": "string" + }, + { + "name": "metadata.project", + "description": "Project. Project of the resource", + "in": "query", + "required": false, + "type": "string" + }, + { + "name": "metadata.organization", + "description": "Organization. Organization to which the resource belongs", + "in": "query", + "required": false, + "type": "string" + }, + { + "name": "metadata.partner", + "description": "Partner. Partner to which the resource belongs", + "in": "query", + "required": false, + "type": "string" + }, + { + "name": "metadata.modifiedAt", + "in": "query", + "required": false, + "type": "string", + "format": "date-time" + }, + { + "name": "spec.idpName", + "in": "query", + "required": false, + "type": "string" + }, + { + "name": "spec.domain", + "in": "query", + "required": false, + "type": "string" + }, + { + "name": "spec.acsUrl", + "in": "query", + "required": false, + "type": "string" + }, + { + "name": "spec.ssoUrl", + "in": "query", + "required": false, + "type": "string" + }, + { + "name": "spec.idpCert", + "in": "query", + "required": false, + "type": "string" + }, + { + "name": "spec.spCert", + "in": "query", + "required": false, + "type": "string" + }, + { + "name": "spec.metadataUrl", + "in": "query", + "required": false, + "type": "string" + }, + { + "name": "spec.metadataFilename", + "in": "query", + "required": false, + "type": "string" + }, + { + "name": "spec.saeEnabled", + "in": "query", + "required": false, + "type": "boolean" + }, + { + "name": "spec.groupAttributeName", + "in": "query", + "required": false, + "type": "string" + }, + { + "name": "spec.nameIdFormat", + "in": "query", + "required": false, + "type": "string" + }, + { + "name": "spec.consumerBinding", + "in": "query", + "required": false, + "type": "string" + }, + { + "name": "spec.spEntityId", + "in": "query", + "required": false, + "type": "string" + }, + { + "name": "status.conditionType", + "description": "Condition Type. type of the status condition", + "in": "query", + "required": false, + "type": "string" + }, + { + "name": "status.conditionStatus", + "description": "Condition Status. status of the condition", + "in": "query", + "required": false, + "type": "string", + "enum": [ + "StatusNotSet", + "StatusSubmitted", + "StatusOK", + "StatusFailed" + ], + "default": "StatusNotSet" + }, + { + "name": "status.lastUpdated", + "description": "Last Updated. when the condition status is last updated", + "in": "query", + "required": false, + "type": "string", + "format": "date-time" + }, + { + "name": "status.reason", + "description": "Reason. reason of the last condition status", + "in": "query", + "required": false, + "type": "string" + } + ], + "tags": [ + "Idp" + ] + }, + "put": { + "operationId": "Idp_UpdateIdp", + "responses": { + "200": { + "description": "A successful response.", + "schema": { + "$ref": "#/definitions/v3Idp" + } + }, + "403": { + "description": "Returned when the user does not have permission to access the resource.", + "schema": {} + }, + "404": { + "description": "Returned when the resource does not exist.", + "schema": { + "type": "string", + "format": "string" + } + }, + "default": { + "description": "An unexpected error response.", + "schema": { + "$ref": "#/definitions/googlerpcStatus" + } + } + }, + "parameters": [ + { + "name": "metadata.id", + "in": "path", + "required": true, + "type": "string" + }, + { + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/v3Idp" + } + } + ], + "tags": [ + "Idp" + ] + } + } + }, + "definitions": { + "commonv3Status": { + "type": "object", + "properties": { + "conditionType": { + "type": "string", + "description": "type of the status condition", + "title": "Condition Type", + "readOnly": true + }, + "conditionStatus": { + "$ref": "#/definitions/v3ConditionStatus", + "enum": [ + "StatusNotSet", + "StatusSubmitted", + "StatusOK", + "StatusFailed" + ], + "description": "status of the condition", + "title": "Condition Status", + "readOnly": true + }, + "lastUpdated": { + "type": "string", + "format": "date-time", + "description": "when the condition status is last updated", + "title": "Last Updated", + "readOnly": true + }, + "reason": { + "type": "string", + "description": "reason of the last condition status", + "title": "Reason", + "readOnly": true + } + }, + "description": "status of a resource", + "title": "Status", + "readOnly": true + }, + "googlerpcStatus": { + "type": "object", + "properties": { + "code": { + "type": "integer", + "format": "int32" + }, + "message": { + "type": "string" + }, + "details": { + "type": "array", + "items": { + "$ref": "#/definitions/protobufAny" + } + } + } + }, + "protobufAny": { + "type": "object", + "properties": { + "@type": { + "type": "string" + } + }, + "additionalProperties": {} + }, + "v3ConditionStatus": { + "type": "string", + "enum": [ + "StatusNotSet", + "StatusSubmitted", + "StatusOK", + "StatusFailed" + ], + "default": "StatusNotSet", + "title": "$title: ConditionStatus\n$description: status of a condition for a resource" + }, + "v3Idp": { + "type": "object", + "properties": { + "apiVersion": { + "type": "string", + "default": "usermgmt.k8smgmt.io/v3", + "description": "API Version of the idp resource", + "title": "API Version" + }, + "kind": { + "type": "string", + "default": "Idp", + "description": "Kind of the idp resource", + "title": "Kind" + }, + "metadata": { + "$ref": "#/definitions/v3Metadata", + "description": "Metadata of the idp resource", + "title": "Metadata" + }, + "spec": { + "$ref": "#/definitions/v3IdpSpec", + "description": "Spec of the idp resource", + "title": "Spec" + }, + "status": { + "$ref": "#/definitions/commonv3Status", + "description": "Status of the resource", + "title": "Status", + "readOnly": true + } + }, + "description": "Idp", + "title": "Idp", + "required": [ + "apiVersion", + "kind", + "metadata", + "spec" + ] + }, + "v3IdpList": { + "type": "object", + "properties": { + "apiVersion": { + "type": "string", + "default": "usermgmt.k8smgmt.io/v3", + "description": "API Version of the idp list resource", + "title": "API Version", + "readOnly": true + }, + "kind": { + "type": "string", + "default": "IdpList", + "description": "Kind of the idp list resource", + "title": "Kind", + "readOnly": true + }, + "metadata": { + "$ref": "#/definitions/v3ListMetadata", + "description": "Metadata of the idp list resource", + "title": "Metadata", + "readOnly": true + }, + "items": { + "type": "array", + "items": { + "$ref": "#/definitions/v3Idp" + }, + "description": "List of the idp resources", + "title": "Items", + "readOnly": true + } + }, + "description": "idp list", + "title": "IdpList", + "readOnly": true + }, + "v3IdpSpec": { + "type": "object", + "properties": { + "idpName": { + "type": "string" + }, + "domain": { + "type": "string" + }, + "acsUrl": { + "type": "string" + }, + "ssoUrl": { + "type": "string" + }, + "idpCert": { + "type": "string" + }, + "spCert": { + "type": "string" + }, + "metadataUrl": { + "type": "string" + }, + "metadataFilename": { + "type": "string" + }, + "saeEnabled": { + "type": "boolean" + }, + "groupAttributeName": { + "type": "string" + }, + "nameIdFormat": { + "type": "string" + }, + "consumerBinding": { + "type": "string" + }, + "spEntityId": { + "type": "string" + } + } + }, + "v3ListMetadata": { + "type": "object", + "properties": { + "count": { + "type": "string", + "format": "int64" + }, + "offset": { + "type": "string", + "format": "int64" + }, + "limt": { + "type": "string", + "format": "int64" + } + }, + "title": "$title: ListMetadata\n$description: metadata for a list of resources\n$required: enabled" + }, + "v3Metadata": { + "type": "object", + "example": { + "name": "some-name", + "project": "defaultproject" + }, + "properties": { + "name": { + "type": "string", + "description": "name of the resource", + "title": "Name" + }, + "description": { + "type": "string", + "description": "description of the resource", + "title": "Description" + }, + "labels": { + "type": "object", + "additionalProperties": { + "type": "string" + }, + "description": "labels of the resource", + "title": "Lables" + }, + "annotations": { + "type": "object", + "additionalProperties": { + "type": "string" + }, + "description": "annotations of the resource", + "title": "Annotations" + }, + "project": { + "type": "string", + "description": "Project of the resource", + "title": "Project" + }, + "organization": { + "type": "string", + "description": "Organization to which the resource belongs", + "title": "Organization" + }, + "partner": { + "type": "string", + "description": "Partner to which the resource belongs", + "title": "Partner" + }, + "id": { + "type": "string", + "readOnly": true + }, + "modifiedAt": { + "type": "string", + "format": "date-time", + "readOnly": true + } + }, + "description": "metadata of the resource", + "title": "Metadata", + "required": [ + "name", + "project" + ] + } + }, + "securityDefinitions": { + "ApiKeyAuth": { + "type": "apiKey", + "name": "X-RAFAY-API-KEYID", + "in": "header" + }, + "BasicAuth": { + "type": "basic" + } + }, + "security": [ + { + "ApiKeyAuth": [], + "BasicAuth": [] + } + ] +} diff --git a/components/usermgmt/gen/openapi/proto/types/userpb/v3/idp.swagger.json b/components/usermgmt/gen/openapi/proto/types/userpb/v3/idp.swagger.json new file mode 100644 index 0000000..6a8e370 --- /dev/null +++ b/components/usermgmt/gen/openapi/proto/types/userpb/v3/idp.swagger.json @@ -0,0 +1,43 @@ +{ + "swagger": "2.0", + "info": { + "title": "proto/types/userpb/v3/idp.proto", + "version": "version not set" + }, + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "paths": {}, + "definitions": { + "googlerpcStatus": { + "type": "object", + "properties": { + "code": { + "type": "integer", + "format": "int32" + }, + "message": { + "type": "string" + }, + "details": { + "type": "array", + "items": { + "$ref": "#/definitions/protobufAny" + } + } + } + }, + "protobufAny": { + "type": "object", + "properties": { + "@type": { + "type": "string" + } + }, + "additionalProperties": {} + } + } +} diff --git a/components/usermgmt/go.mod b/components/usermgmt/go.mod index 5945c4c..fb6aff5 100644 --- a/components/usermgmt/go.mod +++ b/components/usermgmt/go.mod @@ -5,6 +5,7 @@ go 1.17 require ( github.com/RafaySystems/rcloud-base/components/adminsrv v0.0.0-unpublished github.com/RafaySystems/rcloud-base/components/common v0.0.0-unpublished + github.com/crewjam/saml v0.4.6 github.com/gogo/protobuf v1.3.2 github.com/google/uuid v1.3.0 github.com/grpc-ecosystem/grpc-gateway/v2 v2.7.2 @@ -21,20 +22,27 @@ require ( ) require ( + github.com/beevik/etree v1.1.0 // indirect + github.com/crewjam/httperr v0.2.0 // indirect github.com/fatih/color v1.13.0 // indirect github.com/fsnotify/fsnotify v1.5.1 // indirect + github.com/golang-jwt/jwt/v4 v4.1.0 // indirect github.com/golang/protobuf v1.5.2 // indirect github.com/grpc-ecosystem/grpc-gateway v1.16.0 // indirect github.com/hashicorp/hcl v1.0.0 // indirect github.com/jinzhu/inflection v1.0.0 // indirect + github.com/jonboulle/clockwork v0.2.2 // indirect github.com/julienschmidt/httprouter v1.3.0 // indirect github.com/klauspost/cpuid/v2 v2.0.6 // indirect github.com/magiconair/properties v1.8.5 // indirect + github.com/mattermost/xml-roundtrip-validator v0.1.0 // indirect github.com/mattn/go-colorable v0.1.12 // indirect github.com/mattn/go-isatty v0.0.14 // indirect github.com/mitchellh/mapstructure v1.4.3 // indirect github.com/pelletier/go-toml v1.9.4 // indirect + github.com/pkg/errors v0.9.1 // indirect github.com/processout/grpc-go-pool v1.2.1 // indirect + github.com/russellhaering/goxmldsig v1.1.1 // indirect github.com/segmentio/asm v1.1.0 // indirect github.com/segmentio/encoding v0.3.2 // indirect github.com/speps/go-hashids v2.0.0+incompatible // indirect diff --git a/components/usermgmt/go.sum b/components/usermgmt/go.sum index fda661d..a845f90 100644 --- a/components/usermgmt/go.sum +++ b/components/usermgmt/go.sum @@ -76,6 +76,8 @@ github.com/armon/go-metrics v0.3.10/go.mod h1:4O98XIr/9W0sxpJ8UaYkvjk10Iff7SnFrb github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= github.com/armon/go-radix v1.0.0/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= github.com/asaskevich/govalidator v0.0.0-20190424111038-f61b66f89f4a/go.mod h1:lB+ZfQJz7igIIfQNfa7Ml4HSf2uFQQRzpGGRXenZAgY= +github.com/beevik/etree v1.1.0 h1:T0xke/WvNtMoCqgzPhkX2r4rjY3GDZFi+FjpRZY2Jbs= +github.com/beevik/etree v1.1.0/go.mod h1:r8Aw8JqVegEf0w2fDnATrX9VpkMcyFeM0FhwO62wh+A= github.com/benbjohnson/clock v1.0.3/go.mod h1:bGMdMPoPVvcYyt1gHDf4J2KE153Yf9BuiUKYMaxlTDM= github.com/benbjohnson/clock v1.1.0 h1:Q92kusRqC1XV2MjkWETPvjJVqKetz1OzxZB7mHJLju8= github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA= @@ -122,9 +124,14 @@ github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfc github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/creack/pty v1.1.11/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= +github.com/crewjam/httperr v0.2.0 h1:b2BfXR8U3AlIHwNeFFvZ+BV1LFvKLlzMjzaTnZMybNo= +github.com/crewjam/httperr v0.2.0/go.mod h1:Jlz+Sg/XqBQhyMjdDiC+GNNRzZTD7x39Gu3pglZ5oH4= +github.com/crewjam/saml v0.4.6 h1:XCUFPkQSJLvzyl4cW9OvpWUbRf0gE7VUpU8ZnilbeM4= +github.com/crewjam/saml v0.4.6/go.mod h1:ZBOXnNPFzB3CgOkRm7Nd6IVdkG+l/wF+0ZXLqD96t1A= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/dchest/uniuri v0.0.0-20200228104902-7aecb25e1fe5/go.mod h1:GgB8SF9nRG+GqaDtLcwJZsQFhcogVCJ79j4EdT0c2V4= github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no= github.com/docopt/docopt-go v0.0.0-20180111231733-ee0de3bc6815/go.mod h1:WwZ+bS3ebgob9U8Nd0kOddGdZWjyMGR8Wziv+TBNwSE= @@ -186,6 +193,8 @@ github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zV github.com/gogo/protobuf v1.3.1/go.mod h1:SlYgWuQ5SjCEi6WLHjHCa1yvBfUnHcTbrrZtXPKa29o= github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= +github.com/golang-jwt/jwt/v4 v4.1.0 h1:XUgk2Ex5veyVFVeLm0xhusUTQybEbexJXrvPNOKkSY0= +github.com/golang-jwt/jwt/v4 v4.1.0/go.mod h1:/xlHOz8bRuivTWchD4jCa+NbatV+wEUSzwAxVc6locg= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= github.com/golang/glog v1.0.0 h1:nfP3RFugxnNRyKgeWd4oI1nYvXpxrx8ck8ZrcizshdQ= github.com/golang/glog v1.0.0/go.mod h1:EWib/APOK0SL3dFbYqvxE3UYd8E6s1ouQ7iEp/0LWV4= @@ -328,6 +337,7 @@ github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJS github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E= github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc= github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo= +github.com/jonboulle/clockwork v0.2.2 h1:UOGuzwb1PwsrDAObMuhUnj0p5ULPj8V/xJ7Kx9qUBdQ= github.com/jonboulle/clockwork v0.2.2/go.mod h1:Pkfl5aHPm1nk2H9h0bjmnJD/BcgbGXUBGnn1kMkgxc8= github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4= @@ -354,6 +364,9 @@ github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg= github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= +github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= +github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0= +github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= @@ -365,6 +378,8 @@ github.com/magiconair/properties v1.8.5/go.mod h1:y3VJvCyxH9uVvJTWEGAELF3aiYNyPK github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= github.com/mailru/easyjson v0.7.6/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= +github.com/mattermost/xml-roundtrip-validator v0.1.0 h1:RXbVD2UAl7A7nOTR4u7E3ILa4IbtvKBHw64LDsmu9hU= +github.com/mattermost/xml-roundtrip-validator v0.1.0/go.mod h1:qccnGMcpgwcNaBnxqpJpWWUiPNr5H3O8eDgGV9gT5To= github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= github.com/mattn/go-colorable v0.1.6/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= @@ -407,7 +422,6 @@ github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8m github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f/go.mod h1:ZdcZmHo+o7JKHSa8/e818NopupXU1YMK5fe1lsApnBw= -github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs= github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE= @@ -435,6 +449,7 @@ github.com/pelletier/go-toml v1.9.3/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCko github.com/pelletier/go-toml v1.9.4 h1:tjENF6MfZAg8e4ZmZTeWaWiT2vXtsoO6+iuOjFhECwM= github.com/pelletier/go-toml v1.9.4/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c= github.com/peterbourgon/diskv v2.0.1+incompatible/go.mod h1:uqqh8zWWbv1HBMNONnaR/tNboyR3/BZd58JJSHlUSCU= +github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= @@ -474,6 +489,11 @@ github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40T github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg= github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ= github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= +github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= +github.com/rogpeppe/go-internal v1.8.0 h1:FCbCCtXNOY3UtUuHUYaghJg4y7Fd14rXifAYUAtL9R8= +github.com/rogpeppe/go-internal v1.8.0/go.mod h1:WmiCO8CzOY8rg0OYDC4/i/2WRWAB6poM+XZ2dLUbcbE= +github.com/russellhaering/goxmldsig v1.1.1 h1:vI0r2osGF1A9PLvsGdPUAGwEIrKa4Pj5sesSBsebIxM= +github.com/russellhaering/goxmldsig v1.1.1/go.mod h1:gM4MDENBQf7M+V824SGfyIUVFWydB7n0KkEubVJl+Tw= github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts= github.com/sagikazarmark/crypt v0.4.0/go.mod h1:ALv2SRj7GxYV4HO9elxH9nS6M9gW+xDNxqmyJ6RfDFM= @@ -552,6 +572,7 @@ github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9de github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= github.com/yuin/goldmark v1.4.0/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= +github.com/zenazn/goji v1.0.1/go.mod h1:7S9M489iMyHBNxwZnk9/EHS098H4/F6TATF2mIxtB1Q= go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU= go.etcd.io/bbolt v1.3.6/go.mod h1:qXsaaIqmgQH0T+OPdb99Bf+PKfBBQVAdyD6TY9G8XM4= go.etcd.io/etcd/api/v3 v3.5.0/go.mod h1:cbVKeC6lCfl7j/8jBhAK6aIYO9XOjdptoxU/nLQcPvs= @@ -609,6 +630,7 @@ golang.org/x/crypto v0.0.0-20190923035154-9ee001bba392/go.mod h1:/lpIB1dKB+9EgE3 golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20201002170205-7f63de1d35b0/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4= golang.org/x/crypto v0.0.0-20210817164053-32db794688a5/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20211215153901-e495a2d5b3d3 h1:0es+/5331RGQPcXlMfP+WrnIIS6dNnNRe0WB02W0F4M= golang.org/x/crypto v0.0.0-20211215153901-e495a2d5b3d3/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= @@ -1057,8 +1079,9 @@ gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLks gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f h1:BLraFXnmrev5lT+xlilqcH8XK9/i0At2xKjWk4p6zsU= gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= @@ -1085,6 +1108,8 @@ gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b h1:h8qDotaEPuJATrMmW04NCwg7v22aHH28wwpauUhK9Oo= gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gotest.tools v2.2.0+incompatible h1:VsBPFP1AI068pPrMxtb/S8Zkgf9xEmTLJjfM+P5UIEo= +gotest.tools v2.2.0+incompatible/go.mod h1:DsYFclhRJ6vuDpmuTbkuFWG+y2sxOXAzmJt81HFBacw= gotest.tools/v3 v3.0.2/go.mod h1:3SzNCllyD9/Y+b5r9JIKQ474KzkZyqLqEfYqMsX94Bk= gotest.tools/v3 v3.0.3/go.mod h1:Z7Lb0S5l+klDB31fvDQX8ss/FlKDxtlFlw3Oa8Ymbl8= honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= diff --git a/components/usermgmt/main.go b/components/usermgmt/main.go index 972c445..22754e3 100644 --- a/components/usermgmt/main.go +++ b/components/usermgmt/main.go @@ -61,6 +61,7 @@ var ( us service.UserService gs service.GroupService rs service.RoleService + is service.IdpService dev bool _log = logv2.GetLogger() authPool authv3.AuthPool @@ -122,6 +123,7 @@ func setup() { us = service.NewUserService(kc, db) gs = service.NewGroupService(db) rs = service.NewRoleService(db) + is = service.NewIdpService(db) _log.Infow("usermgmt setup complete") } @@ -155,6 +157,7 @@ func runAPI(wg *sync.WaitGroup, ctx context.Context) { pbrpcv3.RegisterUserHandlerFromEndpoint, pbrpcv3.RegisterGroupHandlerFromEndpoint, pbrpcv3.RegisterRoleHandlerFromEndpoint, + pbrpcv3.RegisterIdpHandlerFromEndpoint, ) if err != nil { _log.Fatalw("unable to create gateway", "error", err) @@ -188,6 +191,7 @@ func runRPC(wg *sync.WaitGroup, ctx context.Context) { userServer := server.NewUserServer(us) groupServer := server.NewGroupServer(gs) roleServer := server.NewRoleServer(rs) + idpServer := server.NewIdpServer(is) l, err := net.Listen("tcp", fmt.Sprintf(":%d", rpcPort)) if err != nil { @@ -224,6 +228,7 @@ func runRPC(wg *sync.WaitGroup, ctx context.Context) { rpcv3.RegisterUserServer(s, userServer) rpcv3.RegisterGroupServer(s, groupServer) rpcv3.RegisterRoleServer(s, roleServer) + rpcv3.RegisterIdpServer(s, idpServer) _log.Infow("starting rpc server", "port", rpcPort) err = s.Serve(l) diff --git a/components/usermgmt/pkg/internal/models/idp.go b/components/usermgmt/pkg/internal/models/idp.go new file mode 100644 index 0000000..01d1df3 --- /dev/null +++ b/components/usermgmt/pkg/internal/models/idp.go @@ -0,0 +1,34 @@ +package models + +import ( + "time" + + "github.com/google/uuid" + "github.com/uptrace/bun" +) + +type Idp struct { + bun.BaseModel `bun:"table:authsrv_idp,alias:idp"` + + Id uuid.UUID `bun:"id,type:uuid,pk,default:uuid_generate_v4()"` + Name string `bun:"name,notnull,unique"` + Description string `bun:"description"` + CreatedAt time.Time `bun:"created_at,notnull,default:current_timestamp"` + ModifiedAt time.Time `bun:"modified_at,notnull,default:current_timestamp"` + + IdpName string `bun:"idp_name,notnull"` + Domain string `bun:"domain,notnull,unique"` + AcsURL string `bun:"acs_url,notnull,unique"` + OrganizationId uuid.UUID `bun:"organization_id,type:uuid"` + PartnerId uuid.UUID `bun:"partner_id,type:uuid"` + SsoURL string `bun:"sso_url"` + IdpCert string `bun:"idp_cert"` + SpCert string `bun:"sp_cert"` + SpKey string `bun:"sp_key"` + MetadataURL string `bun:"metadata_url"` + MetadataFilename string `bun:"metadata_filename"` + Metadata []byte `bun:"metadata"` + GroupAttributeName string `bun:"group_attribute_name"` + SaeEnabled bool `bun:"is_sae_enabled"` + Trash bool `bun:"trash,default:false"` +} diff --git a/components/usermgmt/pkg/server/idp.go b/components/usermgmt/pkg/server/idp.go new file mode 100644 index 0000000..56116e6 --- /dev/null +++ b/components/usermgmt/pkg/server/idp.go @@ -0,0 +1,38 @@ +package server + +import ( + "context" + + "github.com/RafaySystems/rcloud-base/components/usermgmt/pkg/service" + rpcv3 "github.com/RafaySystems/rcloud-base/components/usermgmt/proto/rpc/v3" + userv3 "github.com/RafaySystems/rcloud-base/components/usermgmt/proto/types/userpb/v3" + "google.golang.org/protobuf/types/known/emptypb" +) + +type idpServer struct { + service.IdpService +} + +func NewIdpServer(is service.IdpService) rpcv3.IdpServer { + return &idpServer{is} +} + +func (s *idpServer) CreateIdp(ctx context.Context, idp *userv3.Idp) (*userv3.Idp, error) { + return s.IdpService.Create(ctx, idp) +} + +func (s *idpServer) GetIdp(ctx context.Context, idp *userv3.Idp) (*userv3.Idp, error) { + return s.IdpService.GetByID(ctx, idp) +} + +func (s *idpServer) ListIdps(ctx context.Context, _ *emptypb.Empty) (*userv3.IdpList, error) { + return s.IdpService.List(ctx) +} + +func (s *idpServer) UpdateIdp(ctx context.Context, idp *userv3.Idp) (*userv3.Idp, error) { + return s.IdpService.Update(ctx, idp) +} + +func (s *idpServer) DeleteIdp(ctx context.Context, idpID *userv3.Idp) (*emptypb.Empty, error) { + return &emptypb.Empty{}, s.IdpService.Delete(ctx, idpID) +} diff --git a/components/usermgmt/pkg/service/idp.go b/components/usermgmt/pkg/service/idp.go new file mode 100644 index 0000000..f5fc417 --- /dev/null +++ b/components/usermgmt/pkg/service/idp.go @@ -0,0 +1,385 @@ +package service + +import ( + "bytes" + "context" + "crypto/rand" + "crypto/rsa" + "crypto/x509" + "crypto/x509/pkix" + "encoding/pem" + "fmt" + "io/ioutil" + "math/big" + "net/url" + "os" + "time" + + "github.com/RafaySystems/rcloud-base/components/common/pkg/persistence/provider/pg" + commonv3 "github.com/RafaySystems/rcloud-base/components/common/proto/types/commonpb/v3" + "github.com/RafaySystems/rcloud-base/components/usermgmt/pkg/internal/models" + userv3 "github.com/RafaySystems/rcloud-base/components/usermgmt/proto/types/userpb/v3" + "github.com/google/uuid" + "github.com/uptrace/bun" +) + +var baseUrl *url.URL + +func init() { + base, ok := os.LookupEnv("APP_HOST_HTTP") + if !ok || len(base) == 0 { + panic("APP_HOST_HTTP env not set") + } + var err error + baseUrl, err = url.Parse(base) + if err != nil { + panic("Failed to get application url") + } +} + +type IdpService interface { + Create(context.Context, *userv3.Idp) (*userv3.Idp, error) + GetByID(context.Context, *userv3.Idp) (*userv3.Idp, error) + List(context.Context) (*userv3.IdpList, error) + Update(context.Context, *userv3.Idp) (*userv3.Idp, error) + Delete(context.Context, *userv3.Idp) error +} + +type idpService struct { + dao pg.EntityDAO +} + +func NewIdpService(db *bun.DB) IdpService { + return &idpService{ + dao: pg.NewEntityDAO(db), + } +} + +func generateAcsURL() (string, error) { + uuid := uuid.New() + return fmt.Sprintf("%s/%s/", baseUrl.String(), uuid.String()), nil +} + +// generateSpCert generates self signed certificate. Returns cert and +// private key. +func generateSpCert(host string) (string, string, error) { + // generate private key of type rsa + priv, err := rsa.GenerateKey(rand.Reader, 4096) + if err != nil { + return "", "", err + } + privPEM := new(bytes.Buffer) + err = pem.Encode(privPEM, &pem.Block{ + Type: "RSA PRIVATE KEY", + Bytes: x509.MarshalPKCS1PrivateKey(priv), + }) + if err != nil { + return "", "", err + } + privPEMBytes, err := ioutil.ReadAll(privPEM) + if err != nil { + return "", "", err + } + + template := &x509.Certificate{ + SerialNumber: big.NewInt(1000), + Subject: pkix.Name{ + Organization: []string{"Rafay"}, + Country: []string{"US"}, + }, + NotBefore: time.Now(), + NotAfter: time.Now().AddDate(30, 0, 0), + ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth}, + KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageKeyEncipherment, + DNSNames: []string{host}, + } + // generate self sign certificate + cBytes, err := x509.CreateCertificate(rand.Reader, template, template, &priv.PublicKey, priv) + if err != nil { + return "", "", err + } + cPEM := new(bytes.Buffer) + err = pem.Encode(cPEM, &pem.Block{ + Type: "CERTIFICATE", + Bytes: cBytes, + }) + if err != nil { + return "", "", err + } + cPEMBytes, err := ioutil.ReadAll(cPEM) + if err != nil { + return "", "", err + } + + return string(cPEMBytes), string(privPEMBytes), nil +} + +func (s *idpService) Create(ctx context.Context, idp *userv3.Idp) (*userv3.Idp, error) { + name := idp.Metadata.GetName() + domain := idp.Spec.GetDomain() + + // validate name and domain + if len(name) == 0 { + return &userv3.Idp{}, fmt.Errorf("EMPTY NAME") + } + if len(domain) == 0 { + return &userv3.Idp{}, fmt.Errorf("EMPTY DOMAIN") + } + e := &models.Idp{} + s.dao.GetByName(ctx, name, e) + if e.Name == name { + return &userv3.Idp{}, fmt.Errorf("DUPLICATE NAME") + } + s.dao.GetX(ctx, "domain", domain, e) + if e.Domain == domain { + return &userv3.Idp{}, fmt.Errorf("DUPLICATE DOMAIN") + } + + acsURL, err := generateAcsURL() + if err != nil { + return &userv3.Idp{}, err + } + entity := &models.Idp{ + Name: name, + Description: idp.Metadata.GetDescription(), + CreatedAt: time.Now(), + IdpName: idp.Spec.GetIdpName(), + Domain: domain, + AcsURL: acsURL, + SsoURL: idp.Spec.GetSsoUrl(), + IdpCert: idp.Spec.GetIdpCert(), + MetadataURL: idp.Spec.GetMetadataUrl(), + MetadataFilename: idp.Spec.GetMetadataFilename(), + GroupAttributeName: idp.Spec.GetGroupAttributeName(), + SaeEnabled: idp.Spec.GetSaeEnabled(), + } + if entity.SaeEnabled { + spcert, spkey, err := generateSpCert(baseUrl.Host) + if err != nil { + return &userv3.Idp{}, err + } + entity.SpCert = spcert + entity.SpKey = spkey + } + _, err = s.dao.Create(ctx, entity) + if err != nil { + return &userv3.Idp{}, err + } + + rv := &userv3.Idp{ + ApiVersion: "usermgmt.k8smgmt.io/v3", + Kind: "Idp", + Metadata: &commonv3.Metadata{ + Name: entity.Name, + Organization: entity.OrganizationId.String(), + Partner: entity.PartnerId.String(), + Id: entity.Id.String(), + }, + Spec: &userv3.IdpSpec{ + IdpName: entity.IdpName, + Domain: entity.Domain, + AcsUrl: entity.AcsURL, + SsoUrl: entity.SsoURL, + IdpCert: entity.IdpCert, + SpCert: entity.SpCert, + MetadataUrl: entity.MetadataURL, + MetadataFilename: entity.MetadataFilename, + SaeEnabled: entity.SaeEnabled, + GroupAttributeName: entity.GroupAttributeName, + NameIdFormat: "Email Address", + ConsumerBinding: "HTTP-POST", + SpEntityId: entity.AcsURL, + }, + } + return rv, nil +} + +func (s *idpService) GetByID(ctx context.Context, idp *userv3.Idp) (*userv3.Idp, error) { + id, err := uuid.Parse(idp.Metadata.GetId()) + if err != nil { + return &userv3.Idp{}, err + } + entity := &models.Idp{} + // TODO: Check for existance of id before GetByID + _, err = s.dao.GetByID(ctx, id, entity) + if err != nil { + return &userv3.Idp{}, err + } + rv := &userv3.Idp{ + ApiVersion: "usermgmt.k8smgmt.io/v3", + Kind: "Idp", + Metadata: &commonv3.Metadata{ + Name: entity.Name, + Organization: entity.OrganizationId.String(), + Partner: entity.PartnerId.String(), + Id: entity.Id.String(), + }, + Spec: &userv3.IdpSpec{ + IdpName: entity.IdpName, + Domain: entity.Domain, + AcsUrl: entity.AcsURL, + SsoUrl: entity.SsoURL, + IdpCert: entity.IdpCert, + SpCert: entity.SpCert, + MetadataUrl: entity.MetadataURL, + MetadataFilename: entity.MetadataFilename, + SaeEnabled: entity.SaeEnabled, + GroupAttributeName: entity.GroupAttributeName, + NameIdFormat: "Email Address", + ConsumerBinding: "HTTP-POST", + SpEntityId: entity.AcsURL, + }, + } + return rv, nil +} + +func (s *idpService) Update(ctx context.Context, idp *userv3.Idp) (*userv3.Idp, error) { + var id, orgId, partId uuid.UUID + id, err := uuid.Parse(idp.Metadata.GetId()) + // TODO: 400 Bad Request + if err != nil { + return &userv3.Idp{}, err + } + if len(idp.Metadata.GetOrganization()) != 0 { + orgId, err = uuid.Parse(idp.Metadata.GetOrganization()) + if err != nil { + return &userv3.Idp{}, err + } + } + if len(idp.Metadata.GetPartner()) != 0 { + partId, err = uuid.Parse(idp.Metadata.GetPartner()) + if err != nil { + return &userv3.Idp{}, err + } + } + _, err = s.dao.GetByID(ctx, id, &models.Idp{}) + // TODO: Return proper error for Id not exist + if err != nil { + return &userv3.Idp{}, err + } + + entity := &models.Idp{ + Id: id, + Name: idp.Metadata.GetName(), + Description: idp.Metadata.GetDescription(), + ModifiedAt: time.Now(), + IdpName: idp.Spec.GetIdpName(), + Domain: idp.Spec.GetDomain(), + AcsURL: idp.Spec.GetAcsUrl(), + OrganizationId: orgId, + PartnerId: partId, + SsoURL: idp.Spec.GetSsoUrl(), + IdpCert: idp.Spec.GetIdpCert(), + MetadataURL: idp.Spec.GetMetadataUrl(), + MetadataFilename: idp.Spec.GetMetadataFilename(), + GroupAttributeName: idp.Spec.GetGroupAttributeName(), + SaeEnabled: idp.Spec.GetSaeEnabled(), + } + if entity.SaeEnabled { + spcert, spkey, err := generateSpCert(baseUrl.Host) + if err != nil { + return &userv3.Idp{}, err + } + entity.SpCert = spcert + entity.SpKey = spkey + } + + _, err = s.dao.Update(ctx, id, entity) + if err != nil { + return &userv3.Idp{}, err + } + rv := &userv3.Idp{ + ApiVersion: "usermgmt.k8smgmt.io/v3", + Kind: "Idp", + Metadata: &commonv3.Metadata{ + Name: entity.Name, + Organization: entity.OrganizationId.String(), + Partner: entity.PartnerId.String(), + Id: entity.Id.String(), + }, + Spec: &userv3.IdpSpec{ + IdpName: entity.IdpName, + Domain: entity.Domain, + AcsUrl: entity.AcsURL, + SsoUrl: entity.SsoURL, + IdpCert: entity.IdpCert, + SpCert: entity.SpCert, + MetadataUrl: entity.MetadataURL, + MetadataFilename: entity.MetadataFilename, + SaeEnabled: entity.SaeEnabled, + GroupAttributeName: entity.GroupAttributeName, + NameIdFormat: "Email Address", + ConsumerBinding: "HTTP-POST", + SpEntityId: entity.AcsURL, + }, + } + return rv, nil +} + +func (s *idpService) List(ctx context.Context) (*userv3.IdpList, error) { + var ( + entities []models.Idp + orgID uuid.NullUUID + parID uuid.NullUUID + ) + _, err := s.dao.List(ctx, parID, orgID, &entities) + if err != nil { + return &userv3.IdpList{}, err + } + + // Get idps only till limit + var result []*userv3.Idp + for _, entity := range entities { + e := &userv3.Idp{ + ApiVersion: "usermgmt.k8smgmt.io/v3", + Kind: "Idp", + Metadata: &commonv3.Metadata{ + Name: entity.Name, + Organization: entity.OrganizationId.String(), + Partner: entity.PartnerId.String(), + Id: entity.Id.String(), + }, + Spec: &userv3.IdpSpec{ + IdpName: entity.IdpName, + Domain: entity.Domain, + AcsUrl: entity.AcsURL, + SsoUrl: entity.SsoURL, + IdpCert: entity.IdpCert, + SpCert: entity.SpCert, + MetadataUrl: entity.MetadataURL, + MetadataFilename: entity.MetadataFilename, + SaeEnabled: entity.SaeEnabled, + GroupAttributeName: entity.GroupAttributeName, + NameIdFormat: "Email Address", + ConsumerBinding: "HTTP-POST", + SpEntityId: entity.AcsURL, + }, + } + result = append(result, e) + } + + rv := &userv3.IdpList{ + ApiVersion: "usermgmt.k8smgmt.io/v3", + Kind: "IdpList", + Items: result, + } + return rv, nil +} + +func (s *idpService) Delete(ctx context.Context, idp *userv3.Idp) error { + id, err := uuid.Parse(idp.Metadata.GetId()) + if err != nil { + return err + } + entity := &models.Idp{} + _, err = s.dao.GetByID(ctx, id, entity) + if entity.Id != id { + return fmt.Errorf("ID DOES NOT EXISTS") + } + + err = s.dao.Delete(ctx, id, &models.Idp{}) + if err != nil { + return err + } + return nil +} diff --git a/components/usermgmt/pkg/sso/saml/middleware.go b/components/usermgmt/pkg/sso/saml/middleware.go new file mode 100644 index 0000000..624fa71 --- /dev/null +++ b/components/usermgmt/pkg/sso/saml/middleware.go @@ -0,0 +1,167 @@ +package saml + +import ( + "context" + "crypto/rsa" + "crypto/tls" + "crypto/x509" + "net/http" + "net/url" + "os" + "strings" + + "github.com/RafaySystems/rcloud-base/components/usermgmt/pkg/internal/models" + "github.com/crewjam/saml" + "github.com/crewjam/saml/samlsp" +) + +func newSAMLMiddlewareFromIDP(idp models.Idp) (*SAMLMiddleware, error) { + rootURL, err := url.Parse(os.Getenv("APP_HOST_HTTP")) + if err != nil { + return nil, err + } + + var idpMetadata *saml.EntityDescriptor + if len(idp.Metadata) == 0 { + idpMetadataURL, err := url.Parse(idp.MetadataURL) + if err != nil { + return nil, err + } + idpMetadata, err = samlsp.FetchMetadata(context.Background(), http.DefaultClient, + *idpMetadataURL) + } else { + idpMetadata, err = samlsp.ParseMetadata(idp.Metadata) + if err != nil { + return nil, err + } + } + + acsURL, err := url.Parse(idp.AcsURL) + if err != nil { + return nil, err + } + + keyPair, err := tls.X509KeyPair([]byte(idp.SpCert), []byte(idp.SpKey)) + if err != nil { + return nil, err + } + keyPair.Leaf, err = x509.ParseCertificate(keyPair.Certificate[0]) + if err != nil { + return nil, err + } + + opts := samlsp.Options{ + EntityID: "", + URL: *rootURL, + Key: keyPair.PrivateKey.(*rsa.PrivateKey), + Certificate: keyPair.Leaf, + AllowIDPInitiated: false, + DefaultRedirectURI: "/prelogin", + IDPMetadata: idpMetadata, + SignRequest: false, + } + sp := samlsp.DefaultServiceProvider(opts) + sp.AcsURL = *acsURL + m := &samlsp.Middleware{ + ServiceProvider: sp, + Binding: "", + ResponseBinding: saml.HTTPPostBinding, + OnError: samlsp.DefaultOnError, + Session: samlsp.DefaultSessionProvider(opts), + } + m.RequestTracker = samlsp.DefaultRequestTracker(opts, &m.ServiceProvider) + if opts.UseArtifactResponse { + m.ResponseBinding = saml.HTTPArtifactBinding + } + return &SAMLMiddleware{m}, nil +} + +// SAMLAuth is an authentication middleware. +func (s *SAMLService) SAMLAuth(handler http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + err := r.ParseForm() + if err != nil { + http.Error(w, "failed to parse form data", http.StatusBadRequest) + return + } + username := r.PostForm.Get("username") + + if !strings.Contains(username, "@") { + http.Error(w, "Invalid email address", http.StatusBadRequest) + return + } + domain := strings.SplitN(username, "@", 2)[1] + + entity, err := s.EntityDAO.GetX(context.Background(), "domain", domain, &models.Idp{}) + if err != nil { + http.Error(w, "No idp found for domain", http.StatusInternalServerError) + return + } + idp, ok := entity.(models.Idp) + if !ok { + http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) + return + } + + m, err := newSAMLMiddlewareFromIDP(idp) + if err != nil { + http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) + return + } + + session, err := m.Session.GetSession(r) + if session != nil { + r = r.WithContext(samlsp.ContextWithSession(r.Context(), session)) + w.Write([]byte("authentiated successfully")) + return + } + if err == samlsp.ErrNoSession { + m.HandleStartAuthFlow(w, r) + return + } else { + http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) + return + } + }) +} + +// ServeACS performs SAML Response assertions. +func (s *SAMLService) ServeACS(w http.ResponseWriter, r *http.Request) { + r.ParseForm() + base, _ := url.Parse(os.Getenv("APP_HOST_HTTP")) + acsURL := base.ResolveReference(r.URL) + + entity, err := s.EntityDAO.GetX(context.Background(), "acs_url", acsURL.String(), &models.Idp{}) + if err != nil { + http.Error(w, "No Idp for ACS URL", http.StatusInternalServerError) + return + } + idp, ok := entity.(models.Idp) + if !ok { + http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) + return + } + + m, err := newSAMLMiddlewareFromIDP(idp) + if err != nil { + http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) + return + } + + possibleRequestIDs := []string{} + if m.ServiceProvider.AllowIDPInitiated { + possibleRequestIDs = append(possibleRequestIDs, "") + } + + trackedRequests := m.RequestTracker.GetTrackedRequests(r) + for _, tr := range trackedRequests { + possibleRequestIDs = append(possibleRequestIDs, tr.SAMLRequestID) + } + assertion, err := m.ServiceProvider.ParseResponse(r, possibleRequestIDs) + if err != nil { + m.OnError(w, r, err) + return + } + m.CreateSessionFromAssertion(w, r, assertion, m.ServiceProvider.DefaultRedirectURI) + return +} diff --git a/components/usermgmt/pkg/sso/saml/saml.go b/components/usermgmt/pkg/sso/saml/saml.go new file mode 100644 index 0000000..d4aebf0 --- /dev/null +++ b/components/usermgmt/pkg/sso/saml/saml.go @@ -0,0 +1,21 @@ +package saml + +import ( + pg "github.com/RafaySystems/rcloud-base/components/common/pkg/persistence/provider/pg" + "github.com/crewjam/saml/samlsp" + "github.com/uptrace/bun" +) + +type SAMLMiddleware struct { + *samlsp.Middleware +} + +type SAMLService struct { + EntityDAO pg.EntityDAO +} + +func NewSAMLService(db *bun.DB) *SAMLService { + return &SAMLService{ + EntityDAO: pg.NewEntityDAO(db), + } +} diff --git a/components/usermgmt/pkg/sso/saml/saml_test.go b/components/usermgmt/pkg/sso/saml/saml_test.go new file mode 100644 index 0000000..3db4e72 --- /dev/null +++ b/components/usermgmt/pkg/sso/saml/saml_test.go @@ -0,0 +1 @@ +package saml diff --git a/components/usermgmt/proto/rpc/v3/idp.proto b/components/usermgmt/proto/rpc/v3/idp.proto new file mode 100644 index 0000000..8ebe111 --- /dev/null +++ b/components/usermgmt/proto/rpc/v3/idp.proto @@ -0,0 +1,109 @@ +syntax = "proto3"; +package rafay.dev.rpc.v3; + +import "google/api/annotations.proto"; +import "protoc-gen-openapiv2/options/annotations.proto"; +import "proto/types/userpb/v3/idp.proto"; +import "google/protobuf/empty.proto"; + +option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_swagger) = { + info : { + title : "Identity Provider (IdP) Service" + version : "2.0" + contact : {name : "Rafay Dev"} + } + schemes : HTTPS + consumes : "application/json" + consumes : "application/yaml" + produces : "application/json" + produces : "application/yaml" + security_definitions : { + security : { + key : "BasicAuth" + value : {type : TYPE_BASIC} + } + security : { + key : "ApiKeyAuth" + value : {type : TYPE_API_KEY in : IN_HEADER name : "X-RAFAY-API-KEYID"} + } + } + security : { + security_requirement : { + key : "BasicAuth" + value : {} + } + security_requirement : { + key : "ApiKeyAuth" + value : {} + } + } + responses : { + key : "403" + value : { + description : "Returned when the user does not have permission to access " + "the resource." + } + } + responses : { + key : "404" + value : { + description : "Returned when the resource does not exist." + schema : {json_schema : {type : STRING}} + } + } +}; + + +service Idp { + rpc CreateIdp(rafay.dev.types.user.v3.Idp) returns (rafay.dev.types.user.v3.Idp) { + option (google.api.http) = { + post : "/auth/v3/sso/idp" + body : "*" + }; + + option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { + responses : { + key : "201" + value : {description : "Returned when idp is created successfully."} + } + }; + }; + + rpc GetIdp(rafay.dev.types.user.v3.Idp) returns (rafay.dev.types.user.v3.Idp) { + option (google.api.http) = { + get : "/auth/v3/sso/idp/{metadata.id}" + }; + }; + + rpc ListIdps(google.protobuf.Empty) returns (rafay.dev.types.user.v3.IdpList) { + option (google.api.http) = { + get : "/auth/v3/sso/idp" + }; + }; + + rpc UpdateIdp(rafay.dev.types.user.v3.Idp) returns (rafay.dev.types.user.v3.Idp) { + option (google.api.http) = { + put : "/auth/v3/sso/idp/{metadata.id}" + body : "*" + }; + }; + + rpc DeleteIdp(rafay.dev.types.user.v3.Idp) returns (google.protobuf.Empty) { + option (google.api.http) = { + delete : "/auth/v3/sso/idp/{metadata.id}" + }; + + option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { + responses : { + key : "204" + value : { + description : "Returned when idp is deleted successfully." + } + } + }; + }; + + // TODO: endpooint /auth/v1/sso/idp/{id}/upload_metadata/ + // file content as request payload and response is Idp +} + diff --git a/components/usermgmt/proto/types/userpb/v3/idp.proto b/components/usermgmt/proto/types/userpb/v3/idp.proto new file mode 100644 index 0000000..37bbef1 --- /dev/null +++ b/components/usermgmt/proto/types/userpb/v3/idp.proto @@ -0,0 +1,97 @@ +syntax = "proto3"; +package rafay.dev.types.user.v3; + +import "proto/types/commonpb/v3/common.proto"; +import "protoc-gen-openapiv2/options/annotations.proto"; + +message Idp { + option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_schema) = { + json_schema : { + title : "Idp" + description : "Idp" + required : [ "apiVersion", "kind", "metadata", "spec" ] + } + }; + + string apiVersion = 1 + [ (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + title : "API Version", + description : "API Version of the idp resource" + default : "usermgmt.k8smgmt.io/v3" + } ]; + string kind = 2 + [ (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + title : "Kind", + description : "Kind of the idp resource" + default : "Idp" + } ]; + rafay.dev.types.common.v3.Metadata metadata = 3 + [ (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + title : "Metadata", + description : "Metadata of the idp resource" + } ]; + IdpSpec spec = 4 + [ (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + title : "Spec", + description : "Spec of the idp resource" + } ]; + + rafay.dev.types.common.v3.Status status = 5 + [ (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + title : "Status", + description : "Status of the resource" + read_only : true + } ]; +} + +message IdpSpec { + string idpName = 1; + string domain = 2; + string acsUrl = 3; + string ssoUrl = 4; + string idpCert = 5; + string spCert = 6; + string metadataUrl = 7; + string metadataFilename = 8; + bool saeEnabled = 9; + string groupAttributeName = 10; + string nameIdFormat = 11; + string consumerBinding = 12; + string spEntityId = 13; +} + +message IdpList { + option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_schema) = { + json_schema : { + title : "IdpList" + description : "idp list" + read_only : true + } + }; + string apiVersion = 1 + [ (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + title : "API Version", + description : "API Version of the idp list resource" + default : "usermgmt.k8smgmt.io/v3" + read_only : true + } ]; + string kind = 2 + [ (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + title : "Kind", + description : "Kind of the idp list resource" + default : "IdpList" + read_only : true + } ]; + rafay.dev.types.common.v3.ListMetadata metadata = 3 + [ (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + title : "Metadata", + description : "Metadata of the idp list resource" + read_only : true + } ]; + repeated Idp items = 4 + [ (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + title : "Items", + description : "List of the idp resources" + read_only : true + } ]; +}